├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── AddToContextMenu.reg ├── LICENSE ├── README.md ├── images ├── ContextMenu.png ├── Result.png ├── Result2.png ├── RunAsAdmin.png └── Window.png ├── intag.sln └── intag ├── App.config ├── ConsoleWindow.cs ├── Constants.cs ├── EncodingUtils.cs ├── Extensions.cs ├── FileUtils.cs ├── HRes.cs ├── IPropertyStore.cs ├── IShellItem2.cs ├── ParentProcessUtilities.cs ├── Program.cs ├── PropVar.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx ├── Settings.Designer.cs ├── Settings.settings ├── app.manifest └── launchSettings.json ├── PropertyKey.cs ├── RegUtils.cs ├── Resources ├── add.png └── remove.png ├── ShEnums.cs ├── System └── Windows │ └── Forms │ ├── RoundedButton.cs │ └── RoundedTextBox.cs ├── WindowUtils.cs ├── intag - Backup.csproj ├── intag.csproj ├── intag_32.ico ├── intag_512.ico ├── intag_form.Designer.cs ├── intag_form.cs └── intag_form.resx /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: jamminroot 2 | ko_fi: jamminroot 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/dotnetcore,vs,rider,windows,certificates 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=dotnetcore,vs,rider,windows,certificates 4 | 5 | ### certificates ### 6 | *.pem 7 | *.key 8 | *.crt 9 | *.cer 10 | *.priv 11 | 12 | ### DotnetCore ### 13 | # .NET Core build folders 14 | bin/ 15 | obj/ 16 | 17 | # Common node modules locations 18 | /node_modules 19 | /wwwroot/node_modules 20 | 21 | ### Rider ### 22 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 23 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 24 | 25 | # User-specific stuff 26 | .idea 27 | 28 | # Gradle and Maven with auto-import 29 | # When using Gradle or Maven with auto-import, you should exclude module files, 30 | # since they will be recreated, and may cause churn. Uncomment if using 31 | # auto-import. 32 | # .idea/artifacts 33 | # .idea/compiler.xml 34 | # .idea/jarRepositories.xml 35 | # .idea/modules.xml 36 | # .idea/*.iml 37 | # .idea/modules 38 | # *.iml 39 | # *.ipr 40 | 41 | # CMake 42 | cmake-build-*/ 43 | 44 | # Mongo Explorer plugin 45 | .idea/**/mongoSettings.xml 46 | 47 | # File-based project format 48 | *.iws 49 | 50 | # IntelliJ 51 | out/ 52 | 53 | # mpeltonen/sbt-idea plugin 54 | .idea_modules/ 55 | 56 | # JIRA plugin 57 | atlassian-ide-plugin.xml 58 | 59 | # Cursive Clojure plugin 60 | .idea/replstate.xml 61 | 62 | # Crashlytics plugin (for Android Studio and IntelliJ) 63 | com_crashlytics_export_strings.xml 64 | crashlytics.properties 65 | crashlytics-build.properties 66 | fabric.properties 67 | 68 | # Editor-based Rest Client 69 | .idea/httpRequests 70 | 71 | # Android studio 3.1+ serialized cache file 72 | .idea/caches/build_file_checksums.ser 73 | 74 | ### vs ### 75 | ## Ignore Visual Studio temporary files, build results, and 76 | ## files generated by popular Visual Studio add-ons. 77 | ## 78 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 79 | 80 | # User-specific files 81 | *.rsuser 82 | *.suo 83 | *.user 84 | *.userosscache 85 | *.sln.docstates 86 | 87 | # User-specific files (MonoDevelop/Xamarin Studio) 88 | *.userprefs 89 | 90 | # Mono auto generated files 91 | mono_crash.* 92 | 93 | # Build results 94 | [Dd]ebug/ 95 | [Dd]ebugPublic/ 96 | [Rr]elease/ 97 | [Rr]eleases/ 98 | x64/ 99 | x86/ 100 | [Aa][Rr][Mm]/ 101 | [Aa][Rr][Mm]64/ 102 | bld/ 103 | [Bb]in/ 104 | [Oo]bj/ 105 | [Ll]og/ 106 | [Ll]ogs/ 107 | 108 | # Visual Studio 2015/2017 cache/options directory 109 | .vs/ 110 | # Uncomment if you have tasks that create the project's static files in wwwroot 111 | #wwwroot/ 112 | 113 | # Visual Studio 2017 auto generated files 114 | Generated\ Files/ 115 | 116 | # MSTest test Results 117 | [Tt]est[Rr]esult*/ 118 | [Bb]uild[Ll]og.* 119 | 120 | # NUnit 121 | *.VisualState.xml 122 | TestResult.xml 123 | nunit-*.xml 124 | 125 | # Build Results of an ATL Project 126 | [Dd]ebugPS/ 127 | [Rr]eleasePS/ 128 | dlldata.c 129 | 130 | # Benchmark Results 131 | BenchmarkDotNet.Artifacts/ 132 | 133 | # .NET Core 134 | project.lock.json 135 | project.fragment.lock.json 136 | artifacts/ 137 | 138 | # StyleCop 139 | StyleCopReport.xml 140 | 141 | # Files built by Visual Studio 142 | *_i.c 143 | *_p.c 144 | *_h.h 145 | *.ilk 146 | *.meta 147 | *.obj 148 | *.iobj 149 | *.pch 150 | *.pdb 151 | *.ipdb 152 | *.pgc 153 | *.pgd 154 | *.rsp 155 | *.sbr 156 | *.tlb 157 | *.tli 158 | *.tlh 159 | *.tmp 160 | *.tmp_proj 161 | *_wpftmp.csproj 162 | *.log 163 | *.vspscc 164 | *.vssscc 165 | .builds 166 | *.pidb 167 | *.svclog 168 | *.scc 169 | 170 | # Chutzpah Test files 171 | _Chutzpah* 172 | 173 | # Visual C++ cache files 174 | ipch/ 175 | *.aps 176 | *.ncb 177 | *.opendb 178 | *.opensdf 179 | *.sdf 180 | *.cachefile 181 | *.VC.db 182 | *.VC.VC.opendb 183 | 184 | # Visual Studio profiler 185 | *.psess 186 | *.vsp 187 | *.vspx 188 | *.sap 189 | 190 | # Visual Studio Trace Files 191 | *.e2e 192 | 193 | # TFS 2012 Local Workspace 194 | $tf/ 195 | 196 | # Guidance Automation Toolkit 197 | *.gpState 198 | 199 | # ReSharper is a .NET coding add-in 200 | _ReSharper*/ 201 | *.[Rr]e[Ss]harper 202 | *.DotSettings.user 203 | 204 | # TeamCity is a build add-in 205 | _TeamCity* 206 | 207 | # DotCover is a Code Coverage Tool 208 | *.dotCover 209 | 210 | # AxoCover is a Code Coverage Tool 211 | .axoCover/* 212 | !.axoCover/settings.json 213 | 214 | # Coverlet is a free, cross platform Code Coverage Tool 215 | coverage*[.json, .xml, .info] 216 | 217 | # Visual Studio code coverage results 218 | *.coverage 219 | *.coveragexml 220 | 221 | # NCrunch 222 | _NCrunch_* 223 | .*crunch*.local.xml 224 | nCrunchTemp_* 225 | 226 | # MightyMoose 227 | *.mm.* 228 | AutoTest.Net/ 229 | 230 | # Web workbench (sass) 231 | .sass-cache/ 232 | 233 | # Installshield output folder 234 | [Ee]xpress/ 235 | 236 | # DocProject is a documentation generator add-in 237 | DocProject/buildhelp/ 238 | DocProject/Help/*.HxT 239 | DocProject/Help/*.HxC 240 | DocProject/Help/*.hhc 241 | DocProject/Help/*.hhk 242 | DocProject/Help/*.hhp 243 | DocProject/Help/Html2 244 | DocProject/Help/html 245 | 246 | # Click-Once directory 247 | publish/ 248 | 249 | # Publish Web Output 250 | *.[Pp]ublish.xml 251 | *.azurePubxml 252 | # Note: Comment the next line if you want to checkin your web deploy settings, 253 | # but database connection strings (with potential passwords) will be unencrypted 254 | *.pubxml 255 | *.publishproj 256 | 257 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 258 | # checkin your Azure Web App publish settings, but sensitive information contained 259 | # in these scripts will be unencrypted 260 | PublishScripts/ 261 | 262 | # NuGet Packages 263 | *.nupkg 264 | # NuGet Symbol Packages 265 | *.snupkg 266 | # The packages folder can be ignored because of Package Restore 267 | **/[Pp]ackages/* 268 | # except build/, which is used as an MSBuild target. 269 | !**/[Pp]ackages/build/ 270 | # Uncomment if necessary however generally it will be regenerated when needed 271 | #!**/[Pp]ackages/repositories.config 272 | # NuGet v3's project.json files produces more ignorable files 273 | *.nuget.props 274 | *.nuget.targets 275 | 276 | # Microsoft Azure Build Output 277 | csx/ 278 | *.build.csdef 279 | 280 | # Microsoft Azure Emulator 281 | ecf/ 282 | rcf/ 283 | 284 | # Windows Store app package directories and files 285 | AppPackages/ 286 | BundleArtifacts/ 287 | Package.StoreAssociation.xml 288 | _pkginfo.txt 289 | *.appx 290 | *.appxbundle 291 | *.appxupload 292 | 293 | # Visual Studio cache files 294 | # files ending in .cache can be ignored 295 | *.[Cc]ache 296 | # but keep track of directories ending in .cache 297 | !?*.[Cc]ache/ 298 | 299 | # Others 300 | ClientBin/ 301 | ~$* 302 | *~ 303 | *.dbmdl 304 | *.dbproj.schemaview 305 | *.jfm 306 | *.pfx 307 | *.publishsettings 308 | orleans.codegen.cs 309 | 310 | # Including strong name files can present a security risk 311 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 312 | #*.snk 313 | 314 | # Since there are multiple workflows, uncomment next line to ignore bower_components 315 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 316 | #bower_components/ 317 | 318 | # RIA/Silverlight projects 319 | Generated_Code/ 320 | 321 | # Backup & report files from converting an old project file 322 | # to a newer Visual Studio version. Backup files are not needed, 323 | # because we have git ;-) 324 | _UpgradeReport_Files/ 325 | Backup*/ 326 | UpgradeLog*.XML 327 | UpgradeLog*.htm 328 | ServiceFabricBackup/ 329 | *.rptproj.bak 330 | 331 | # SQL Server files 332 | *.mdf 333 | *.ldf 334 | *.ndf 335 | 336 | # Business Intelligence projects 337 | *.rdl.data 338 | *.bim.layout 339 | *.bim_*.settings 340 | *.rptproj.rsuser 341 | *- [Bb]ackup.rdl 342 | *- [Bb]ackup ([0-9]).rdl 343 | *- [Bb]ackup ([0-9][0-9]).rdl 344 | 345 | # Microsoft Fakes 346 | FakesAssemblies/ 347 | 348 | # GhostDoc plugin setting file 349 | *.GhostDoc.xml 350 | 351 | # Node.js Tools for Visual Studio 352 | .ntvs_analysis.dat 353 | node_modules/ 354 | 355 | # Visual Studio 6 build log 356 | *.plg 357 | 358 | # Visual Studio 6 workspace options file 359 | *.opt 360 | 361 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 362 | *.vbw 363 | 364 | # Visual Studio LightSwitch build output 365 | **/*.HTMLClient/GeneratedArtifacts 366 | **/*.DesktopClient/GeneratedArtifacts 367 | **/*.DesktopClient/ModelManifest.xml 368 | **/*.Server/GeneratedArtifacts 369 | **/*.Server/ModelManifest.xml 370 | _Pvt_Extensions 371 | 372 | # Paket dependency manager 373 | .paket/paket.exe 374 | paket-files/ 375 | 376 | # FAKE - F# Make 377 | .fake/ 378 | 379 | # CodeRush personal settings 380 | .cr/personal 381 | 382 | # Python Tools for Visual Studio (PTVS) 383 | __pycache__/ 384 | *.pyc 385 | 386 | # Cake - Uncomment if you are using it 387 | # tools/** 388 | # !tools/packages.config 389 | 390 | # Tabs Studio 391 | *.tss 392 | 393 | # Telerik's JustMock configuration file 394 | *.jmconfig 395 | 396 | # BizTalk build output 397 | *.btp.cs 398 | *.btm.cs 399 | *.odx.cs 400 | *.xsd.cs 401 | 402 | # OpenCover UI analysis results 403 | OpenCover/ 404 | 405 | # Azure Stream Analytics local run output 406 | ASALocalRun/ 407 | 408 | # MSBuild Binary and Structured Log 409 | *.binlog 410 | 411 | # NVidia Nsight GPU debugger configuration file 412 | *.nvuser 413 | 414 | # MFractors (Xamarin productivity tool) working folder 415 | .mfractor/ 416 | 417 | # Local History for Visual Studio 418 | .localhistory/ 419 | 420 | # BeatPulse healthcheck temp database 421 | healthchecksdb 422 | 423 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 424 | MigrationBackup/ 425 | 426 | # Ionide (cross platform F# VS Code tools) working folder 427 | .ionide/ 428 | 429 | ### Windows ### 430 | # Windows thumbnail cache files 431 | Thumbs.db 432 | Thumbs.db:encryptable 433 | ehthumbs.db 434 | ehthumbs_vista.db 435 | 436 | # Dump file 437 | *.stackdump 438 | 439 | # Folder config file 440 | [Dd]esktop.ini 441 | 442 | # Recycle Bin used on file shares 443 | $RECYCLE.BIN/ 444 | 445 | # Windows Installer files 446 | *.cab 447 | *.msi 448 | *.msix 449 | *.msm 450 | *.msp 451 | 452 | # Windows shortcuts 453 | *.lnk 454 | 455 | # End of https://www.toptal.com/developers/gitignore/api/dotnetcore,vs,rider,windows,certificates -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/.gitmodules -------------------------------------------------------------------------------- /AddToContextMenu.reg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/AddToContextMenu.reg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dmitrii Chichuk 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 | # InTag 2 | 3 | > POLL📢[VERSION v.2 DISCUSSION AND PLANNING](https://github.com/Jamminroot/intag/discussions/29) 4 | 5 | InTag is a tool that allows you to add tags to folders and files from the explorer context menu. It can also scan nearby files and use their tags for convenience. The tool is designed to be small and lightweight. 6 | 7 | ## Features 8 | 9 | - Add tags to folders and files from the explorer context menu 10 | - Scan nearby files and use their tags for convenience 11 | - Small and lightweight design 12 | - *NEW* - added support of CLI usage 13 | 14 | ## Usage (UI) 15 | 16 | To use InTag, follow these steps: 17 | 18 | 1. Right-click the folder (on Windows 11, you may need to press ' Show more options'), and select InTag. 19 | 20 | ![Context Menu Example](images/ContextMenu.png) 21 | 22 | 2. Assign the desired tags to a folder (or file, if it supports System.Keywords metadata). The neighboring tags will be included in the list of available tags. 23 | 24 | ![Main Window Example](images/Window.png) 25 | 26 | 3. Once you have assigned the tags, click back to the folder or press the enter key to apply the changes. The tags will be assigned, but it may take some time for explorer to detect the changes. To cancel press the esc key. 27 | 28 | NB. The tag input field should be focused and empty for the enter key worked. 29 | 30 | ![Tagged Folders Example](images/Result.png) 31 | 32 | ![Tagged Folders Example - 2](images/Result2.png) 33 | 34 | 4. Ensure that you have enabled grouping by tags by selecting "Context menu on the folder background > group by > More... > Select 'Tags' in the list". 35 | 36 | ## Usage (CLI) 37 | 38 | ```ps1 39 | # Add tags: 40 | .\intag.exe --add "test_tag" --path "C:\path\to\folder\or\file" 41 | # Remove tags: 42 | .\intag.exe --remove "test_tag" --list "C:\path\to\list\file" 43 | # Add And Remove tags (will combine files in list file and argument of "--path"):: 44 | .\intag.exe --add "test_tag" --remove "test_tag" --list "C:\path\to\list\file" --path "C:\path\to\folder\or\file" 45 | ``` 46 | 47 | ## Installation 48 | 49 | There are three methods for installing InTag: 50 | 51 | 1. Automagical: Start the application once, and it should install itself automatically. 52 | 2. Almost automagical: Right-click the application icon in explorer, and run the application with administrator rights. 53 | 54 | ![Run as admin](images/RunAsAdmin.png) 55 | 56 | 3. Manually: Place the .exe file in a directory of your choice, or add a registry entry. 57 | 58 | ## Registry Entry Snippets 59 | 60 | To add the registry entry, use the following code snippets: 61 | 62 | For (only) folders: 63 | ```reg 64 | Windows Registry Editor Version 5.00 65 | 66 | [HKEY_CLASSES_ROOT\Folder\shell\InTag] 67 | "Icon"="\"C:\\\\intag.exe\"" 68 | 69 | [HKEY_CLASSES_ROOT\Folder\shell\InTag\command] 70 | @="\"C:\\\\intag.exe\" --ui --path \"%1\"" 71 | ``` 72 | 73 | For all files: 74 | ```reg 75 | Windows Registry Editor Version 5.00 76 | 77 | [HKEY_CLASSES_ROOT\*\shell\InTag] 78 | "Icon"="\"C:\\\\intag.exe\"" 79 | 80 | [HKEY_CLASSES_ROOT\*\shell\InTag\command] 81 | @="\"C:\\\\intag.exe\" --ui --path \"%1\"" 82 | ``` 83 | 84 | ## Uninstall 85 | 86 | There are two methods for uninstalling InTag: 87 | 88 | 1. Via argument: Run the exe file with the `--uninstall` or `-u` argument. 89 | 2. Manually: Remove the `HKEY_CLASSES_ROOT\Folder\shell\InTag` and(or) `HKEY_CLASSES_ROOT\*\shell\InTag` entries. 90 | 91 | ## Contributions 92 | 93 | [montoner0](https://github.com/montoner0) - great PR with bunch of improvements and fixes, also nice suggestions 94 | 95 | ## Third-Party Notice 96 | 97 | The code for individual file management was taken from the Windows API Code Pack. 98 | 99 | ## Additional Tool 100 | 101 | For even better organization, check out Multistack Launcher, 102 | -------------------------------------------------------------------------------- /images/ContextMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/images/ContextMenu.png -------------------------------------------------------------------------------- /images/Result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/images/Result.png -------------------------------------------------------------------------------- /images/Result2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/images/Result2.png -------------------------------------------------------------------------------- /images/RunAsAdmin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/images/RunAsAdmin.png -------------------------------------------------------------------------------- /images/Window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/images/Window.png -------------------------------------------------------------------------------- /intag.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31321.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "intag", "intag\intag.csproj", "{60BCA8D2-FF4B-4D4A-8CA8-4D1FF7D21CD6}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {60BCA8D2-FF4B-4D4A-8CA8-4D1FF7D21CD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {60BCA8D2-FF4B-4D4A-8CA8-4D1FF7D21CD6}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {60BCA8D2-FF4B-4D4A-8CA8-4D1FF7D21CD6}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {60BCA8D2-FF4B-4D4A-8CA8-4D1FF7D21CD6}.Release|Any CPU.Build.0 = Release|Any CPU 18 | {60BCA8D2-FF4B-4D4A-8CA8-4D1FF7D21CD6}.Release|Any CPU.Deploy.0 = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(SolutionProperties) = preSolution 21 | HideSolutionNode = FALSE 22 | EndGlobalSection 23 | GlobalSection(ExtensibilityGlobals) = postSolution 24 | SolutionGuid = {EBA54C40-ACEF-4857-925B-35A6D6FC0270} 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /intag/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /intag/ConsoleWindow.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace intag 7 | { 8 | static class ConsoleWindow 9 | { 10 | [DllImport("kernel32.dll", SetLastError = true)] 11 | private static extern bool AllocConsole(); 12 | 13 | [DllImport("kernel32.dll", SetLastError = true)] 14 | private static extern bool FreeConsole(); 15 | 16 | [DllImport("kernel32.dll", SetLastError = true)] 17 | private static extern bool AttachConsole(int dwProcessId); 18 | 19 | private const int ATTACH_PARENT_PROCESS = -1; 20 | 21 | public static void Allocate() 22 | { 23 | if (!AttachConsole(ATTACH_PARENT_PROCESS)) 24 | { 25 | AllocConsole(); 26 | } 27 | 28 | Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }); 29 | Console.SetError(new StreamWriter(Console.OpenStandardError()) { AutoFlush = true }); 30 | Console.SetIn(new StreamReader(Console.OpenStandardInput())); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /intag/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace intag 2 | { 3 | public static class Constants 4 | { 5 | internal const string MagicPropertyName = "Prop5"; 6 | internal const string RegistryName = "InTag"; 7 | internal const string DesktopIni = "Desktop.ini"; 8 | //internal const string CanonicalTagViewAggregateName = "System.Photo.TagViewAggregate"; 9 | internal const string CanonicalKeywordsName = "System.Keywords"; 10 | internal const string MagicGuid = "{F29F85E0-4FF9-1068-AB91-08002B27B3D9}"; 11 | 12 | internal const string MagicPropertyTemplate = MagicPropertyName + "=31, "; 13 | 14 | internal const string MagicSectionTemplate = @" 15 | [" + MagicGuid + @"] 16 | " + MagicPropertyTemplate; 17 | 18 | internal const string EmptyDesktopIniTemplate = @"[ViewState] 19 | Mode= 20 | Vid= 21 | FolderType=Generic 22 | " + MagicSectionTemplate; 23 | } 24 | } -------------------------------------------------------------------------------- /intag/EncodingUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace intag 6 | { 7 | public static class EncodingUtils 8 | { 9 | /// 10 | /// Taken from Dan W's answer: https://stackoverflow.com/a/12853721/9092489 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static Encoding DetectTextEncoding(string filename, out string text, int taster = 1000) 17 | { 18 | byte[] b = File.ReadAllBytes(filename); 19 | 20 | // First check the low hanging fruit by checking if a 21 | // BOM/signature exists (sourced from http://www.unicode.org/faq/utf_bom.html#bom4) 22 | if (b.Length >= 4 && b[0] == 0x00 && b[1] == 0x00 && b[2] == 0xFE && b[3] == 0xFF) { text = Encoding.GetEncoding("utf-32BE").GetString(b, 4, b.Length - 4); return Encoding.GetEncoding("utf-32BE"); } // UTF-32, big-endian 23 | else if (b.Length >= 4 && b[0] == 0xFF && b[1] == 0xFE && b[2] == 0x00 && b[3] == 0x00) { text = Encoding.UTF32.GetString(b, 4, b.Length - 4); return Encoding.UTF32; } // UTF-32, little-endian 24 | else if (b.Length >= 2 && b[0] == 0xFE && b[1] == 0xFF) { text = Encoding.BigEndianUnicode.GetString(b, 2, b.Length - 2); return Encoding.BigEndianUnicode; } // UTF-16, big-endian 25 | else if (b.Length >= 2 && b[0] == 0xFF && b[1] == 0xFE) { text = Encoding.Unicode.GetString(b, 2, b.Length - 2); return Encoding.Unicode; } // UTF-16, little-endian 26 | else if (b.Length >= 3 && b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF) { text = Encoding.UTF8.GetString(b, 3, b.Length - 3); return Encoding.UTF8; } // UTF-8 27 | else if (b.Length >= 3 && b[0] == 0x2b && b[1] == 0x2f && b[2] == 0x76) { text = Encoding.UTF7.GetString(b,3,b.Length-3); return Encoding.UTF7; } // UTF-7 28 | 29 | // If the code reaches here, no BOM/signature was found, so now 30 | // we need to 'taste' the file to see if can manually discover 31 | // the encoding. A high taster value is desired for UTF-8 32 | if (taster == 0 || taster > b.Length) taster = b.Length; // Taster size can't be bigger than the filesize obviously. 33 | 34 | // Some text files are encoded in UTF8, but have no BOM/signature. Hence 35 | // the below manually checks for a UTF8 pattern. This code is based off 36 | // the top answer at: https://stackoverflow.com/questions/6555015/check-for-invalid-utf8 37 | // For our purposes, an unnecessarily strict (and terser/slower) 38 | // implementation is shown at: https://stackoverflow.com/questions/1031645/how-to-detect-utf-8-in-plain-c 39 | // For the below, false positives should be exceedingly rare (and would 40 | // be either slightly malformed UTF-8 (which would suit our purposes 41 | // anyway) or 8-bit extended ASCII/UTF-16/32 at a vanishingly long shot). 42 | int i = 0; 43 | bool utf8 = false; 44 | while (i < taster - 4) 45 | { 46 | if (b[i] <= 0x7F) { i += 1; continue; } // If all characters are below 0x80, then it is valid UTF8, but UTF8 is not 'required' (and therefore the text is more desirable to be treated as the default codepage of the computer). Hence, there's no "utf8 = true;" code unlike the next three checks. 47 | if (b[i] >= 0xC2 && b[i] < 0xE0 && b[i + 1] >= 0x80 && b[i + 1] < 0xC0) { i += 2; utf8 = true; continue; } 48 | if (b[i] >= 0xE0 && b[i] < 0xF0 && b[i + 1] >= 0x80 && b[i + 1] < 0xC0 && b[i + 2] >= 0x80 && b[i + 2] < 0xC0) { i += 3; utf8 = true; continue; } 49 | if (b[i] >= 0xF0 && b[i] < 0xF5 && b[i + 1] >= 0x80 && b[i + 1] < 0xC0 && b[i + 2] >= 0x80 && b[i + 2] < 0xC0 && b[i + 3] >= 0x80 && b[i + 3] < 0xC0) { i += 4; utf8 = true; continue; } 50 | utf8 = false; break; 51 | } 52 | if (utf8 == true) { 53 | text = Encoding.UTF8.GetString(b); 54 | return Encoding.UTF8; 55 | } 56 | 57 | // The next check is a heuristic attempt to detect UTF-16 without a BOM. 58 | // We simply look for zeroes in odd or even byte places, and if a certain 59 | // threshold is reached, the code is 'probably' UF-16. 60 | double threshold = 0.1; // proportion of chars step 2 which must be zeroed to be diagnosed as utf-16. 0.1 = 10% 61 | int count = 0; 62 | for (int n = 0; n < taster; n += 2) if (b[n] == 0) count++; 63 | if (((double)count) / taster > threshold) { text = Encoding.BigEndianUnicode.GetString(b); return Encoding.BigEndianUnicode; } 64 | count = 0; 65 | for (int n = 1; n < taster; n += 2) if (b[n] == 0) count++; 66 | if (((double)count) / taster > threshold) { text = Encoding.Unicode.GetString(b); return Encoding.Unicode; } // (little-endian) 67 | 68 | // Finally, a long shot - let's see if we can find "charset=xyz" or 69 | // "encoding=xyz" to identify the encoding: 70 | for (int n = 0; n < taster-9; n++) 71 | { 72 | if ( 73 | ((b[n + 0] == 'c' || b[n + 0] == 'C') && (b[n + 1] == 'h' || b[n + 1] == 'H') && (b[n + 2] == 'a' || b[n + 2] == 'A') && (b[n + 3] == 'r' || b[n + 3] == 'R') && (b[n + 4] == 's' || b[n + 4] == 'S') && (b[n + 5] == 'e' || b[n + 5] == 'E') && (b[n + 6] == 't' || b[n + 6] == 'T') && (b[n + 7] == '=')) || 74 | ((b[n + 0] == 'e' || b[n + 0] == 'E') && (b[n + 1] == 'n' || b[n + 1] == 'N') && (b[n + 2] == 'c' || b[n + 2] == 'C') && (b[n + 3] == 'o' || b[n + 3] == 'O') && (b[n + 4] == 'd' || b[n + 4] == 'D') && (b[n + 5] == 'i' || b[n + 5] == 'I') && (b[n + 6] == 'n' || b[n + 6] == 'N') && (b[n + 7] == 'g' || b[n + 7] == 'G') && (b[n + 8] == '=')) 75 | ) 76 | { 77 | if (b[n + 0] == 'c' || b[n + 0] == 'C') n += 8; else n += 9; 78 | if (b[n] == '"' || b[n] == '\'') n++; 79 | int oldn = n; 80 | while (n < taster && (b[n] == '_' || b[n] == '-' || (b[n] >= '0' && b[n] <= '9') || (b[n] >= 'a' && b[n] <= 'z') || (b[n] >= 'A' && b[n] <= 'Z'))) 81 | { n++; } 82 | byte[] nb = new byte[n-oldn]; 83 | Array.Copy(b, oldn, nb, 0, n-oldn); 84 | try { 85 | string internalEnc = Encoding.ASCII.GetString(nb); 86 | text = Encoding.GetEncoding(internalEnc).GetString(b); 87 | return Encoding.GetEncoding(internalEnc); 88 | } 89 | catch { break; } // If C# doesn't recognize the name of the encoding, break. 90 | } 91 | } 92 | 93 | // If all else fails, the encoding is probably (though certainly not 94 | // definitely) the user's local codepage! One might present to the user a 95 | // list of alternative encodings as shown here: https://stackoverflow.com/questions/8509339/what-is-the-most-common-encoding-of-each-language 96 | // A full list can be found using Encoding.GetEncodings(); 97 | text = Encoding.Default.GetString(b); 98 | return Encoding.Default; 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /intag/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace intag 4 | { 5 | public static class Extensions 6 | { 7 | public static System.Drawing.Color WithAlpha(this System.Drawing.Color color, byte alpha) 8 | { 9 | return System.Drawing.Color.FromArgb(alpha, color); 10 | } 11 | 12 | public static System.Drawing.Color WithBrightness(this System.Drawing.Color color, float brightness) 13 | { 14 | var clamp = Math.Clamp(brightness, 0, 1); 15 | return System.Drawing.Color.FromArgb(color.A, (int)(color.R * clamp), (int)(color.G * clamp), (int)(color.B * clamp)); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /intag/FileUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using System.Security.Permissions; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | 10 | namespace intag 11 | { 12 | public static class FileUtils 13 | { 14 | static PropertyKey _propertyKey = new ("f29f85e0-4ff9-1068-ab91-08002b27b3d9", 5); 15 | static Guid _shellItem2Guid = new ("7E9FB0D3-919F-4307-AB2E-9B1860310C93"); 16 | static Guid _propStoreGuid = new ("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"); 17 | 18 | public static void AssignTags(Dictionary> objectTags) 19 | { 20 | foreach (var pair in objectTags) 21 | { 22 | var obj = pair.Key; 23 | var tags = pair.Value; 24 | if (File.Exists(obj)) 25 | { 26 | AssignTagsToFile(obj, tags); 27 | } else if (Directory.Exists(obj)) 28 | { 29 | AssignTagsToFolder(obj, tags); 30 | } 31 | } 32 | } 33 | 34 | public static void AddTags(string[] objects, string[] tags) 35 | { 36 | var objectTags = new Dictionary>(); 37 | foreach (var obj in objects) 38 | { 39 | if (File.Exists(obj)) 40 | { 41 | var fileTags = GetFileTags(obj); 42 | fileTags.UnionWith(tags); 43 | objectTags[obj] = fileTags; 44 | } 45 | else if (Directory.Exists(obj)) 46 | { 47 | var folderTags = GetFolderTags(obj); 48 | folderTags.UnionWith(tags); 49 | objectTags[obj] = folderTags; 50 | } 51 | } 52 | AssignTags(objectTags); 53 | } 54 | 55 | public static void RemoveTags(string[] objects, string[] tags) 56 | { 57 | var objectTags = new Dictionary>(); 58 | foreach (var obj in objects) 59 | { 60 | if (File.Exists(obj)) 61 | { 62 | var fileTags = GetFileTags(obj); 63 | fileTags.ExceptWith(tags); 64 | objectTags[obj] = fileTags; 65 | } 66 | else if (Directory.Exists(obj)) 67 | { 68 | var folderTags = GetFolderTags(obj); 69 | folderTags.ExceptWith(tags); 70 | objectTags[obj] = folderTags; 71 | } 72 | } 73 | AssignTags(objectTags); 74 | } 75 | 76 | private static void Unhide(FileInfo fi) 77 | { 78 | var f = new FileIOPermission(FileIOPermissionAccess.AllAccess, fi.FullName); 79 | f.AddPathList(FileIOPermissionAccess.AllAccess, fi.FullName); 80 | if (fi.Exists) { fi.IsReadOnly = false; } 81 | f.Demand(); 82 | if (fi.Exists) 83 | { 84 | fi.Attributes ^= FileAttributes.Hidden | FileAttributes.System; 85 | } 86 | } 87 | private static void Hide(FileInfo fi) 88 | { 89 | fi.Attributes |= FileAttributes.Hidden; 90 | fi.Attributes |= FileAttributes.System; 91 | fi.IsReadOnly = true; 92 | } 93 | public static void AssignTagsToFolder(string folder, SortedSet tags) 94 | { 95 | var desktopIniFilepath = Path.Combine(folder, Constants.DesktopIni); 96 | var contents = PrepareContentsForFolder(folder, string.Join(";", tags)); 97 | var fi = new FileInfo(desktopIniFilepath); 98 | Unhide(fi); 99 | //TODO: Encoding detection for new files - maybe in %APPDATA% or registry?.. 100 | var enc = Encoding.Default; 101 | if (File.Exists(desktopIniFilepath)) 102 | { 103 | enc = EncodingUtils.DetectTextEncoding(desktopIniFilepath, out _); 104 | } 105 | File.WriteAllText(desktopIniFilepath, contents, enc); 106 | Hide(fi); 107 | } 108 | 109 | public static void AssignTagsToFile(string file, SortedSet tags) 110 | { 111 | if (!File.Exists(file)) return; 112 | try 113 | { 114 | if (SHCreateItemFromParsingName(file, IntPtr.Zero, ref _shellItem2Guid, out var shellItem) != 0) throw new SystemException("Failed to create shell item"); 115 | if (PSGetPropertyKeyFromName(Constants.CanonicalKeywordsName, out var propKey) != 0) throw new SystemException("Failed to create property key"); 116 | if (shellItem == null) return; 117 | using var propVar = new PropVar(tags.ToArray()); 118 | if(shellItem.GetPropertyStore(GetPropertyStoreOptions.ReadWrite, ref _propStoreGuid, out var propStore)!=0) throw new SystemException("Failed to get property store"); 119 | propStore.SetValue(ref _propertyKey, propVar); 120 | if (propStore.Commit() != HRes.Ok) throw new SystemException("Failed to save tags"); 121 | 122 | Marshal.ReleaseComObject(propStore); 123 | Marshal.ReleaseComObject(shellItem); 124 | } 125 | catch 126 | { 127 | // ignored 128 | } 129 | } 130 | 131 | public static SortedSet GetFolderTags(string folder) 132 | { 133 | var path = Path.Combine(folder, Constants.DesktopIni); 134 | if (!File.Exists(path) || !IsCorrectSectionPresentInDesktopIni(path)) return new SortedSet(); 135 | var enc = EncodingUtils.DetectTextEncoding(path, out var str); 136 | var lines = str.Split(new []{'\n'}); 137 | foreach (var line in lines) 138 | { 139 | if (!line.StartsWith(Constants.MagicPropertyName, StringComparison.OrdinalIgnoreCase)) continue; 140 | var propertyValue = line.Substring(5).Trim('=', '\t', ' ').Substring(3).Trim(); 141 | return new SortedSet(propertyValue.Split(';') 142 | .Select(s => s.Trim()) 143 | .Where(s => !string.IsNullOrWhiteSpace(s))); 144 | } 145 | return new SortedSet(); 146 | } 147 | 148 | public static string GetRawFolderProperties(string path) 149 | { 150 | if (!File.Exists(path) || !IsCorrectSectionPresentInDesktopIni(path)) return ""; 151 | var enc = EncodingUtils.DetectTextEncoding(path, out var str); 152 | var lines = str.Split(new []{'\n'}); 153 | foreach (var line in lines) 154 | { 155 | if (!line.StartsWith(Constants.MagicPropertyName, StringComparison.OrdinalIgnoreCase)) continue; 156 | var propertyValue = line.Substring(5).Trim('=', '\t', ' ').Substring(3).Trim(); 157 | return propertyValue; 158 | } 159 | return ""; 160 | } 161 | 162 | public static Dictionary> GetObjectsTags(string[] objs) 163 | { 164 | var tags = new Dictionary>(); 165 | foreach (var obj in objs) 166 | { 167 | if (File.Exists(obj)) 168 | { 169 | tags[obj] = GetFileTags(obj); 170 | } 171 | else if (Directory.Exists(obj)) 172 | { 173 | tags[obj] = GetFolderTags(obj); 174 | } 175 | } 176 | return tags; 177 | } 178 | 179 | public static SortedSet GetNearbyTags(string targetObject) 180 | { 181 | var directoryInfo = new DirectoryInfo(targetObject).Parent; 182 | if (directoryInfo == null) return null; 183 | var result = new SortedSet(); 184 | foreach (var child in directoryInfo.GetDirectories()) 185 | { 186 | var propertyValue = GetFolderTags(child.FullName); 187 | if (propertyValue.Count==0) continue; 188 | result.UnionWith(propertyValue); 189 | } 190 | foreach (var file in directoryInfo.GetFiles()) 191 | { 192 | var fileTags = GetFileTags(file.FullName); 193 | if (fileTags.Count==0) continue; 194 | result.UnionWith(fileTags); 195 | } 196 | return result; 197 | } 198 | 199 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true)] 200 | private static extern int PSGetPropertyKeyFromName([In, MarshalAs(UnmanagedType.LPWStr)] string pszCanonicalName, out PropertyKey propkey); 201 | 202 | [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] 203 | private static extern int SHCreateItemFromParsingName( 204 | [MarshalAs(UnmanagedType.LPWStr)] string path, 205 | IntPtr pbc, 206 | ref Guid riid, 207 | [MarshalAs(UnmanagedType.Interface)] out IShellItem2 shellItem); 208 | 209 | private static SortedSet GetFileTags(string file) 210 | { 211 | if (!File.Exists(file)) return new SortedSet(); 212 | try 213 | { 214 | if (SHCreateItemFromParsingName(file, IntPtr.Zero, ref _shellItem2Guid, out var shellItem) != 0) throw new SystemException("Failed to create shell item"); 215 | if (PSGetPropertyKeyFromName(Constants.CanonicalKeywordsName, out var propKey) != 0) throw new SystemException("Failed to create property key"); 216 | if (shellItem == null) return new SortedSet(); 217 | using var propVar = new PropVar(); 218 | shellItem.GetProperty(ref propKey, propVar); 219 | if(shellItem.GetPropertyStore(GetPropertyStoreOptions.BestEffort, ref _propStoreGuid, out var propStore)!=0) throw new SystemException("Failed to get property store"); 220 | propStore.GetValue(ref _propertyKey, propVar); 221 | Marshal.ReleaseComObject(propStore); 222 | Marshal.ReleaseComObject(shellItem); 223 | if (propVar.Value is string[] { Length: > 0 } propValues) return new SortedSet(propValues); 224 | } 225 | catch 226 | { 227 | // ignored 228 | } 229 | return new SortedSet(); 230 | } 231 | 232 | private static bool IsCorrectSectionPresentInDesktopIni(string filePath) 233 | { 234 | var enc = EncodingUtils.DetectTextEncoding(filePath, out _); 235 | return new Regex($@"\[{Constants.MagicGuid}\].*{Constants.MagicPropertyName}", RegexOptions.IgnoreCase | RegexOptions.Singleline).IsMatch(File.ReadAllText(filePath, enc)); 236 | } 237 | 238 | private static string PrepareContentsForFolder(string folder, string propertyValue) 239 | { 240 | return Constants.EmptyDesktopIniTemplate + propertyValue; 241 | } 242 | } 243 | } -------------------------------------------------------------------------------- /intag/HRes.cs: -------------------------------------------------------------------------------- 1 | namespace intag 2 | { 3 | public enum HRes 4 | { 5 | /// S_OK 6 | Ok = 0x0000, 7 | 8 | /// S_FALSE 9 | False = 0x0001, 10 | 11 | /// E_INVALIDARG 12 | InvalidArguments = unchecked((int)0x80070057), 13 | 14 | /// E_OUTOFMEMORY 15 | OutOfMemory = unchecked((int)0x8007000E), 16 | 17 | /// E_NOINTERFACE 18 | NoInterface = unchecked((int)0x80004002), 19 | 20 | /// E_FAIL 21 | Fail = unchecked((int)0x80004005), 22 | 23 | /// E_ELEMENTNOTFOUND 24 | ElementNotFound = unchecked((int)0x80070490), 25 | 26 | /// TYPE_E_ELEMENTNOTFOUND 27 | TypeElementNotFound = unchecked((int)0x8002802B), 28 | 29 | /// NO_OBJECT 30 | NoObject = unchecked((int)0x800401E5), 31 | 32 | /// Win32 Error code: ERROR_CANCELLED 33 | Win32ErrorCanceled = 1223, 34 | 35 | /// ERROR_CANCELLED 36 | Canceled = unchecked((int)0x800704C7), 37 | 38 | /// The requested resource is in use 39 | ResourceInUse = unchecked((int)0x800700AA), 40 | 41 | /// The requested resources is read-only. 42 | AccessDenied = unchecked((int)0x80030005) 43 | } 44 | } -------------------------------------------------------------------------------- /intag/IPropertyStore.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace intag 5 | { 6 | /// A property store 7 | [ComImport] 8 | [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")] 9 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 10 | internal interface IPropertyStore 11 | { 12 | /// Gets the number of properties contained in the property store. 13 | /// 14 | /// 15 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 16 | HRes GetCount([Out] out uint propertyCount); 17 | 18 | /// Get a property key located at a specific index. 19 | /// 20 | /// 21 | /// 22 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 23 | HRes GetAt([In] uint propertyIndex, out PropertyKey key); 24 | 25 | /// Gets the value of a property from the store 26 | /// 27 | /// 28 | /// 29 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 30 | HRes GetValue([In] ref PropertyKey key, [Out] PropVar pv); 31 | 32 | /// Sets the value of a property in the store 33 | /// 34 | /// 35 | /// 36 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), PreserveSig] 37 | HRes SetValue([In] ref PropertyKey key, [In] PropVar pv); 38 | 39 | /// Commits the changes. 40 | /// 41 | [PreserveSig] 42 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 43 | HRes Commit(); 44 | } 45 | } -------------------------------------------------------------------------------- /intag/IShellItem2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using System.Runtime.InteropServices.ComTypes; 5 | using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; 6 | 7 | namespace intag 8 | { 9 | internal enum SICHINTF 10 | { 11 | SICHINT_DISPLAY = 0x00000000, 12 | SICHINT_CANONICAL = 0x10000000, 13 | SICHINT_TEST_FILESYSPATH_IF_NOT_EQUAL = 0x20000000, 14 | SICHINT_ALLFIELDS = unchecked((int)0x80000000) 15 | } 16 | 17 | [ComImport] 18 | [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")] 19 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 20 | internal interface IShellItem 21 | { 22 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 23 | void GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); 24 | 25 | [PreserveSig] 26 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 27 | HRes Compare([In] [MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] SICHINTF hint, out int piOrder); 28 | } 29 | 30 | [ComImport] 31 | [Guid("7E9FB0D3-919F-4307-AB2E-9B1860310C93")] 32 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 33 | internal interface IShellItem2 : IShellItem 34 | { 35 | // Not supported: IBindCtx. 36 | [PreserveSig] 37 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 38 | HRes BindToHandler( 39 | [In] IntPtr pbc, 40 | [In] ref Guid bhid, 41 | [In] ref Guid riid, 42 | [Out, MarshalAs(UnmanagedType.Interface)] out object ppv); 43 | 44 | [PreserveSig] 45 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 46 | new HRes GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); 47 | 48 | [PreserveSig] 49 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 50 | HRes GetDisplayName( 51 | [In] ShellItemDesignNameOptions sigdnName, 52 | [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); 53 | 54 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 55 | void GetAttributes([In] ShellFileGetAttributesOptions sfgaoMask, out ShellFileGetAttributesOptions psfgaoAttribs); 56 | 57 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 58 | void Compare( 59 | [In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, 60 | [In] uint hint, 61 | out int piOrder); 62 | 63 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), PreserveSig] 64 | int GetPropertyStore( 65 | [In] GetPropertyStoreOptions Flags, 66 | [In] ref Guid riid, 67 | [Out, MarshalAs(UnmanagedType.Interface)] out IPropertyStore ppv); 68 | 69 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 70 | void GetPropertyStoreWithCreateObject([In] GetPropertyStoreOptions Flags, [In, MarshalAs(UnmanagedType.IUnknown)] object punkCreateObject, [In] ref Guid riid, out IntPtr ppv); 71 | 72 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 73 | void GetPropertyStoreForKeys([In] ref PropertyKey rgKeys, [In] uint cKeys, [In] GetPropertyStoreOptions Flags, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.IUnknown)] out IPropertyStore ppv); 74 | 75 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 76 | void GetPropertyDescriptionList([In] ref PropertyKey keyType, [In] ref Guid riid, out IntPtr ppv); 77 | 78 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 79 | HRes Update([In, MarshalAs(UnmanagedType.Interface)] IBindCtx pbc); 80 | 81 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 82 | void GetProperty([In] ref PropertyKey key, [Out] PropVar ppropvar); 83 | 84 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 85 | void GetCLSID([In] ref PropertyKey key, out Guid pclsid); 86 | 87 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 88 | void GetFileTime([In] ref PropertyKey key, out System.Runtime.InteropServices.ComTypes.FILETIME pft); 89 | 90 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 91 | void GetInt32([In] ref PropertyKey key, out int pi); 92 | 93 | [PreserveSig] 94 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 95 | HRes GetString([In] ref PropertyKey key, [MarshalAs(UnmanagedType.LPWStr)] out string ppsz); 96 | 97 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 98 | void GetUInt32([In] ref PropertyKey key, out uint pui); 99 | 100 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 101 | void GetUInt64([In] ref PropertyKey key, out ulong pull); 102 | 103 | [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] 104 | void GetBool([In] ref PropertyKey key, out int pf); 105 | } 106 | } -------------------------------------------------------------------------------- /intag/ParentProcessUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace intag 7 | { 8 | ///Taken from https://stackoverflow.com/a/3346055/9092489 9 | /// 10 | /// A utility class to determine a process parent. 11 | /// 12 | [StructLayout(LayoutKind.Sequential)] 13 | public struct ParentProcessUtilities 14 | { 15 | // These members must match PROCESS_BASIC_INFORMATION 16 | internal IntPtr Reserved1; 17 | internal IntPtr PebBaseAddress; 18 | internal IntPtr Reserved2_0; 19 | internal IntPtr Reserved2_1; 20 | internal IntPtr UniqueProcessId; 21 | internal IntPtr InheritedFromUniqueProcessId; 22 | 23 | [DllImport("ntdll.dll")] 24 | private static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformation, 25 | int processInformationLength, out int returnLength); 26 | 27 | /// 28 | /// Gets the parent process of the current process. 29 | /// 30 | /// An instance of the Process class. 31 | public static Process GetParentProcess() 32 | { 33 | return GetParentProcess(Process.GetCurrentProcess().Handle); 34 | } 35 | 36 | /// 37 | /// Gets the parent process of specified process. 38 | /// 39 | /// The process id. 40 | /// An instance of the Process class. 41 | public static Process GetParentProcess(int id) 42 | { 43 | Process process = Process.GetProcessById(id); 44 | return GetParentProcess(process.Handle); 45 | } 46 | 47 | /// 48 | /// Gets the parent process of a specified process. 49 | /// 50 | /// The process handle. 51 | /// An instance of the Process class. 52 | public static Process GetParentProcess(IntPtr handle) 53 | { 54 | ParentProcessUtilities pbi = new ParentProcessUtilities(); 55 | int returnLength; 56 | int status = NtQueryInformationProcess(handle, 0, ref pbi, Marshal.SizeOf(pbi), out returnLength); 57 | if (status != 0) throw new Win32Exception(status); 58 | try 59 | { 60 | return Process.GetProcessById(pbi.InheritedFromUniqueProcessId.ToInt32()); 61 | } 62 | catch (ArgumentException) 63 | { 64 | // not found 65 | return null; 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /intag/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Diagnostics; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.IO; 7 | using System.IO.MemoryMappedFiles; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Windows.Forms; 12 | 13 | namespace intag 14 | { 15 | [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] 16 | internal static class Program 17 | { 18 | private const int MMF_MAX_SIZE = 4096; // 4KB 19 | private const int DefaultLaunchDelay = 300; 20 | 21 | [Conditional("DEBUG")] 22 | private static void Log(string message) 23 | { 24 | Debug.WriteLine(message); 25 | Console.WriteLine(message); 26 | var fi = Path.Combine(new FileInfo(Application.ExecutablePath).Directory.FullName, "intag.log"); 27 | var mtx = new Mutex(false, "InTagLogMutex"); 28 | mtx.WaitOne(); 29 | File.AppendAllText(fi, $"{DateTime.Now:O}\t{message}{Environment.NewLine}"); 30 | mtx.ReleaseMutex(); 31 | } 32 | 33 | private static void ShowUsage() 34 | { 35 | Console.WriteLine("Usage:"); 36 | Console.WriteLine(" intag --add [--add ...] --remove [--remove ...] --path [--path ...] --list "); 37 | Console.WriteLine(" intag --path [--path ...] --list "); 38 | Console.WriteLine("Examples:"); 39 | Console.WriteLine(" intag --add tag1 --add tag2 --remove tag3 --path file1.txt --path folder1"); 40 | Console.WriteLine(" intag --path file1.txt --list filelist.txt"); 41 | } 42 | 43 | private static void HandleExplorerRunNoArgs() 44 | { 45 | if (UACHelper.UACHelper.IsElevated) 46 | { 47 | RegUtils.Install(); 48 | MessageBox.Show("InTag is installed.", "InTag", MessageBoxButtons.OK, MessageBoxIcon.Information); 49 | } 50 | else 51 | { 52 | UACHelper.UACHelper.StartElevated(new ProcessStartInfo(Application.ExecutablePath) 53 | { 54 | UseShellExecute = true 55 | }); 56 | } 57 | } 58 | 59 | private static Dictionary ParseArgs(string[] args) 60 | { 61 | var result = new Dictionary(); 62 | for (var i = 0; i < args.Length; i++) 63 | { 64 | if (args[i].StartsWith("--")) 65 | { 66 | var key = args[i]; 67 | if (i + 1 >= args.Length || args[i + 1].StartsWith("--")) 68 | { 69 | result[key] = string.Empty; 70 | continue; 71 | } 72 | var value = args[++i]; 73 | result[key] = value; 74 | } 75 | } 76 | 77 | return result; 78 | } 79 | 80 | 81 | [STAThread] 82 | private static void Main(string[] args) 83 | { 84 | try 85 | { 86 | var parent = ParentProcessUtilities.GetParentProcess()?.ProcessName ?? "explorer"; // Workaround for elevation 87 | Log("Running from parent: " + parent); 88 | if (parent != "explorer") 89 | { 90 | ConsoleWindow.Allocate(); 91 | } 92 | if (args == null || args.Length == 0) 93 | { 94 | if (parent.Equals("explorer", StringComparison.CurrentCultureIgnoreCase)) 95 | { 96 | Log("Goging to install"); 97 | HandleExplorerRunNoArgs(); 98 | } 99 | else 100 | { 101 | Log("Showing usage"); 102 | ShowUsage(); 103 | } 104 | Environment.Exit(0); 105 | } 106 | 107 | if (args!.Length == 1 && (args[0].Equals("--uninstall", StringComparison.CurrentCultureIgnoreCase) || 108 | args[0].Equals("-u", StringComparison.CurrentCultureIgnoreCase))) 109 | { 110 | HandleIntagUninstallArg(); 111 | Environment.Exit(0); 112 | } 113 | 114 | var parsedArgs = ParseArgs(args); 115 | var isWithTagsModificationFlags = parsedArgs.ContainsKey("--add") || parsedArgs.ContainsKey("--remove"); 116 | var isObjectsNotSpecified = !parsedArgs.ContainsKey("--path") && !parsedArgs.ContainsKey("--list"); 117 | if (isWithTagsModificationFlags && isObjectsNotSpecified && !parsedArgs.ContainsKey("--ui")) 118 | { 119 | ShowUsage(); 120 | Environment.Exit(1); 121 | } 122 | 123 | var objects = parsedArgs.Where(p => p.Key == "--path").Select(p => p.Value); 124 | var joinedList = parsedArgs.Where(p => p.Key == "--list").Select(p => p.Value).SelectMany(File.ReadAllLines).ToList(); 125 | joinedList.AddRange(objects); 126 | 127 | var validObjects = joinedList.Where(arg => Directory.Exists(arg) || File.Exists(arg)).ToArray(); 128 | 129 | if (parsedArgs.ContainsKey("--ui")) 130 | { 131 | HandleUiStart(validObjects); 132 | } 133 | else 134 | { 135 | HandleCliStart(parsedArgs, validObjects); 136 | } 137 | } 138 | catch (Win32Exception e) when (e.NativeErrorCode == 1223) 139 | { 140 | // User has canceled the elevation 141 | } 142 | catch (Exception e) 143 | { 144 | Log($"Caught exception:\n{e}"); 145 | MessageBox.Show(e.Message, "InTag", MessageBoxButtons.OK, MessageBoxIcon.Error); 146 | } 147 | } 148 | 149 | private static bool HandleCliStart(Dictionary parsedArgs, string[] validObjects) 150 | { 151 | var tagsToAdd = parsedArgs.Where(p => p.Key == "--add").Select(p => p.Value).ToArray(); 152 | var tagsToRemove = parsedArgs.Where(p => p.Key == "--remove").Select(p => p.Value).ToArray(); 153 | 154 | if (tagsToAdd.Length == 0 && tagsToRemove.Length == 0) 155 | { 156 | var tagsCollection = FileUtils.GetObjectsTags(validObjects); 157 | System.Console.WriteLine("Tags for objects:"); 158 | System.Console.WriteLine(string.Join(Environment.NewLine, tagsCollection.Select(kv => $"{kv.Key}: {string.Join(", ", kv.Value)}"))); 159 | return false; 160 | } 161 | 162 | if (tagsToAdd.Length > 0) 163 | { 164 | Log("Adding tags: " + string.Join(", ", tagsToAdd)); 165 | FileUtils.AddTags(validObjects, tagsToAdd); 166 | } 167 | 168 | if (tagsToRemove.Length > 0) 169 | { 170 | Log("Removing tags: " + string.Join(", ", tagsToRemove)); 171 | FileUtils.RemoveTags(validObjects, tagsToRemove); 172 | } 173 | 174 | return true; 175 | } 176 | 177 | private static bool HandleUiStart(string[] validObjects) 178 | { 179 | var delay = DefaultLaunchDelay; 180 | 181 | if (validObjects.Length == 0) 182 | { 183 | MessageBox.Show("No existing files or folders are passed in a command line.", "InTag", MessageBoxButtons.OK, MessageBoxIcon.Warning); 184 | Environment.Exit(1); 185 | } 186 | 187 | using var mtx = new Mutex(false, "InTagMutex", out var createdNew); 188 | using var mmf = MemoryMappedFile.CreateOrOpen("InTagMemoryMap", MMF_MAX_SIZE + 4); 189 | 190 | mtx.WaitOne(); 191 | 192 | using (var accessor = mmf.CreateViewAccessor()) 193 | { 194 | var tempArray = new byte[MMF_MAX_SIZE]; 195 | accessor.ReadArray(0, tempArray, 0, MMF_MAX_SIZE); 196 | var existingData = Encoding.UTF8.GetString(tempArray).TrimEnd('\0'); 197 | var newData = string.IsNullOrEmpty(existingData) ? validObjects[0] : existingData + Environment.NewLine + validObjects[0]; 198 | var bytes = Encoding.UTF8.GetBytes(newData); 199 | accessor.WriteArray(0, bytes, 0, bytes.Length); 200 | } 201 | 202 | mtx.ReleaseMutex(); 203 | var ourName = Process.GetCurrentProcess().ProcessName; 204 | 205 | if (createdNew) 206 | { 207 | Log("Launched new instance with valid objects: " + string.Join(", ", validObjects)); 208 | Thread.Sleep(delay); 209 | var start = DateTime.Now; 210 | while (Process.GetProcessesByName(ourName).Length > 1 && DateTime.Now - start < TimeSpan.FromSeconds(5)) 211 | { 212 | Thread.Sleep(100); 213 | } 214 | } 215 | else 216 | { 217 | Log("Appended valid objects to existing instance: " + string.Join(", ", validObjects)); 218 | return true; 219 | } 220 | 221 | string batchData; 222 | using (var accessor = mmf.CreateViewAccessor()) 223 | { 224 | var tempArray = new byte[MMF_MAX_SIZE]; 225 | accessor.ReadArray(0, tempArray, 0, MMF_MAX_SIZE); 226 | batchData = Encoding.UTF8.GetString(tempArray).TrimEnd('\0'); 227 | } 228 | 229 | Log($"Launching with batch: [{batchData}]"); 230 | 231 | // Clear MMF 232 | using (var accessor = mmf.CreateViewAccessor()) 233 | { 234 | var emptyBytes = new byte[MMF_MAX_SIZE]; 235 | accessor.WriteArray(0, emptyBytes, 0, MMF_MAX_SIZE); 236 | } 237 | 238 | Application.EnableVisualStyles(); 239 | Application.SetCompatibleTextRenderingDefault(false); 240 | Application.Run(new MainForm(batchData.Split(new[] { Environment.NewLine }, StringSplitOptions.None))); 241 | return false; 242 | } 243 | 244 | private static void HandleIntagUninstallArg() 245 | { 246 | if (UACHelper.UACHelper.IsElevated) 247 | { 248 | RegUtils.Uninstall(); 249 | MessageBox.Show("InTag is uninstalled.", "InTag", MessageBoxButtons.OK, MessageBoxIcon.Information); 250 | } 251 | else 252 | { 253 | UACHelper.UACHelper.StartElevated(new ProcessStartInfo(Application.ExecutablePath, "-u") 254 | { 255 | UseShellExecute = true 256 | }); 257 | } 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /intag/PropVar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace intag 7 | { 8 | [StructLayout(LayoutKind.Explicit)] 9 | public sealed class PropVar : IDisposable 10 | { 11 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 12 | internal static extern void InitPropVariantFromBooleanVector([In, MarshalAs(UnmanagedType.LPArray)] bool[] prgf, uint cElems, [Out] PropVar ppropvar); 13 | 14 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 15 | internal static extern void InitPropVariantFromDoubleVector([In, Out] double[] prgn, uint cElems, [Out] PropVar propvar); 16 | 17 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 18 | internal static extern void InitPropVariantFromFileTime([In] ref System.Runtime.InteropServices.ComTypes.FILETIME pftIn, [Out] PropVar ppropvar); 19 | 20 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 21 | internal static extern void InitPropVariantFromFileTimeVector([In, Out] System.Runtime.InteropServices.ComTypes.FILETIME[] prgft, uint cElems, [Out] PropVar ppropvar); 22 | 23 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 24 | internal static extern void InitPropVariantFromInt16Vector([In, Out] short[] prgn, uint cElems, [Out] PropVar ppropvar); 25 | 26 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 27 | internal static extern void InitPropVariantFromInt32Vector([In, Out] int[] prgn, uint cElems, [Out] PropVar propVar); 28 | 29 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 30 | internal static extern void InitPropVariantFromInt64Vector([In, Out] long[] prgn, uint cElems, [Out] PropVar ppropvar); 31 | 32 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 33 | internal static extern void InitPropVariantFromPropVariantVectorElem([In] PropVar propvarIn, uint iElem, [Out] PropVar ppropvar); 34 | 35 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 36 | internal static extern void InitPropVariantFromStringVector([In, Out] string[] prgsz, uint cElems, [Out] PropVar ppropvar); 37 | 38 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 39 | internal static extern void InitPropVariantFromUInt16Vector([In, Out] ushort[] prgn, uint cElems, [Out] PropVar ppropvar); 40 | 41 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 42 | internal static extern void InitPropVariantFromUInt32Vector([In, Out] uint[] prgn, uint cElems, [Out] PropVar ppropvar); 43 | 44 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 45 | internal static extern void InitPropVariantFromUInt64Vector([In, Out] ulong[] prgn, uint cElems, [Out] PropVar ppropvar); 46 | 47 | [DllImport("Ole32.dll", PreserveSig = false)] // returns hresult 48 | internal static extern void PropVariantClear([In, Out] PropVar pvar); 49 | 50 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 51 | internal static extern void PropVariantGetBooleanElem([In] PropVar propVar, [In]uint iElem, [Out, MarshalAs(UnmanagedType.Bool)] out bool pfVal); 52 | 53 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 54 | internal static extern void PropVariantGetDoubleElem([In] PropVar propVar, [In] uint iElem, [Out] out double pnVal); 55 | 56 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true)] 57 | [return: MarshalAs(UnmanagedType.I4)] 58 | internal static extern int PropVariantGetElementCount([In] PropVar propVar); 59 | 60 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 61 | internal static extern void PropVariantGetFileTimeElem([In] PropVar propVar, [In] uint iElem, [Out, MarshalAs(UnmanagedType.Struct)] out System.Runtime.InteropServices.ComTypes.FILETIME pftVal); 62 | 63 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 64 | internal static extern void PropVariantGetInt16Elem([In] PropVar propVar, [In] uint iElem, [Out] out short pnVal); 65 | 66 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 67 | internal static extern void PropVariantGetInt32Elem([In] PropVar propVar, [In] uint iElem, [Out] out int pnVal); 68 | 69 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 70 | internal static extern void PropVariantGetInt64Elem([In] PropVar propVar, [In] uint iElem, [Out] out long pnVal); 71 | 72 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 73 | internal static extern void PropVariantGetStringElem([In] PropVar propVar, [In] uint iElem, [MarshalAs(UnmanagedType.LPWStr)] ref string ppszVal); 74 | 75 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 76 | internal static extern void PropVariantGetUInt16Elem([In] PropVar propVar, [In] uint iElem, [Out] out ushort pnVal); 77 | 78 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 79 | internal static extern void PropVariantGetUInt32Elem([In] PropVar propVar, [In] uint iElem, [Out] out uint pnVal); 80 | 81 | [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = false)] 82 | internal static extern void PropVariantGetUInt64Elem([In] PropVar propVar, [In] uint iElem, [Out] out ulong pnVal); 83 | 84 | [DllImport("OleAut32.dll", PreserveSig = false)] // returns hresult 85 | internal static extern IntPtr SafeArrayAccessData(IntPtr psa); 86 | 87 | [DllImport("OleAut32.dll", PreserveSig = true)] // psa is actually returned, not hresult 88 | internal static extern IntPtr SafeArrayCreateVector(ushort vt, int lowerBound, uint cElems); 89 | 90 | [DllImport("OleAut32.dll", PreserveSig = true)] // retuns uint32 91 | internal static extern uint SafeArrayGetDim(IntPtr psa); 92 | 93 | // This decl for SafeArrayGetElement is only valid for cDims==1! 94 | [DllImport("OleAut32.dll", PreserveSig = false)] // returns hresult 95 | [return: MarshalAs(UnmanagedType.IUnknown)] 96 | internal static extern object SafeArrayGetElement(IntPtr psa, ref int rgIndices); 97 | 98 | [DllImport("OleAut32.dll", PreserveSig = false)] // returns hresult 99 | internal static extern int SafeArrayGetLBound(IntPtr psa, uint nDim); 100 | 101 | [DllImport("OleAut32.dll", PreserveSig = false)] // returns hresult 102 | internal static extern int SafeArrayGetUBound(IntPtr psa, uint nDim); 103 | 104 | [DllImport("OleAut32.dll", PreserveSig = false)] // returns hresult 105 | internal static extern void SafeArrayUnaccessData(IntPtr psa); 106 | 107 | [StructLayout(LayoutKind.Sequential)] 108 | private struct Blob 109 | { 110 | public int Number; 111 | public IntPtr Pointer; 112 | } 113 | 114 | // A static dictionary of delegates to get data from array's contained within PropVars 115 | private static Dictionary> _vectorActions = null; 116 | 117 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] 118 | private static Dictionary> GenerateVectorActions() 119 | { 120 | Dictionary> cache = new Dictionary> 121 | { 122 | { 123 | typeof(short), 124 | (pv, array, i) => 125 | { 126 | PropVariantGetInt16Elem(pv, i, out short val); 127 | array.SetValue(val, i); 128 | } 129 | }, 130 | 131 | { 132 | typeof(ushort), 133 | (pv, array, i) => 134 | { 135 | PropVariantGetUInt16Elem(pv, i, out ushort val); 136 | array.SetValue(val, i); 137 | } 138 | }, 139 | 140 | { 141 | typeof(int), 142 | (pv, array, i) => 143 | { 144 | PropVariantGetInt32Elem(pv, i, out int val); 145 | array.SetValue(val, i); 146 | } 147 | }, 148 | 149 | { 150 | typeof(uint), 151 | (pv, array, i) => 152 | { 153 | PropVariantGetUInt32Elem(pv, i, out uint val); 154 | array.SetValue(val, i); 155 | } 156 | }, 157 | 158 | { 159 | typeof(long), 160 | (pv, array, i) => 161 | { 162 | PropVariantGetInt64Elem(pv, i, out long val); 163 | array.SetValue(val, i); 164 | } 165 | }, 166 | 167 | { 168 | typeof(ulong), 169 | (pv, array, i) => 170 | { 171 | PropVariantGetUInt64Elem(pv, i, out ulong val); 172 | array.SetValue(val, i); 173 | } 174 | }, 175 | 176 | { 177 | typeof(DateTime), 178 | (pv, array, i) => 179 | { 180 | PropVariantGetFileTimeElem(pv, i, out System.Runtime.InteropServices.ComTypes.FILETIME val); 181 | 182 | long fileTime = GetFileTimeAsLong(ref val); 183 | 184 | array.SetValue(DateTime.FromFileTime(fileTime), i); 185 | } 186 | }, 187 | 188 | { 189 | typeof(bool), 190 | (pv, array, i) => 191 | { 192 | PropVariantGetBooleanElem(pv, i, out bool val); 193 | array.SetValue(val, i); 194 | } 195 | }, 196 | 197 | { 198 | typeof(double), 199 | (pv, array, i) => 200 | { 201 | PropVariantGetDoubleElem(pv, i, out double val); 202 | array.SetValue(val, i); 203 | } 204 | }, 205 | 206 | { 207 | typeof(float), 208 | (pv, array, i) => // float 209 | { 210 | float[] val = new float[1]; 211 | Marshal.Copy(pv._blob.Pointer, val, (int)i, 1); 212 | array.SetValue(val[0], (int)i); 213 | } 214 | }, 215 | 216 | { 217 | typeof(decimal), 218 | (pv, array, i) => 219 | { 220 | int[] val = new int[4]; 221 | for (int a = 0; a < val.Length; a++) 222 | { 223 | val[a] = Marshal.ReadInt32(pv._blob.Pointer, 224 | (int)i * sizeof(decimal) + a * sizeof(int)); //index * size + offset quarter 225 | } 226 | array.SetValue(new decimal(val), i); 227 | } 228 | }, 229 | 230 | { 231 | typeof(string), 232 | (pv, array, i) => 233 | { 234 | string val = string.Empty; 235 | PropVariantGetStringElem(pv, i, ref val); 236 | array.SetValue(val, i); 237 | } 238 | } 239 | }; 240 | 241 | return cache; 242 | } 243 | 244 | /// Attempts to create a PropVar by finding an appropriate constructor. 245 | /// Object from which PropVar should be created. 246 | public static PropVar FromObject(object value) 247 | { 248 | if (value == null) 249 | { 250 | return new PropVar(); 251 | } 252 | else 253 | { 254 | Func func = GetDynamicConstructor(value.GetType()); 255 | return func(value); 256 | } 257 | } 258 | 259 | // A dictionary and lock to contain compiled expression trees for constructors 260 | private static readonly Dictionary> _cache = new Dictionary>(); 261 | 262 | private static readonly object _padlock = new object(); 263 | 264 | // Retrieves a cached constructor expression. If no constructor has been cached, it attempts to find/add it. If it cannot be found an 265 | // exception is thrown. This method looks for a public constructor with the same parameter type as the object. 266 | private static Func GetDynamicConstructor(Type type) 267 | { 268 | lock (_padlock) 269 | { 270 | // initial check, if action is found, return it 271 | if (!_cache.TryGetValue(type, out var action)) 272 | { 273 | // iterates through all constructors 274 | System.Reflection.ConstructorInfo constructor = typeof(PropVar) 275 | .GetConstructor(new Type[] { type }); 276 | 277 | if (constructor == null) 278 | { 279 | // if the method was not found, throw. 280 | throw new ArgumentException("This Value type is not supported."); 281 | } 282 | else // if the method was found, create an expression to call it. 283 | { 284 | // create parameters to action 285 | ParameterExpression arg = Expression.Parameter(typeof(object), "arg"); 286 | 287 | // create an expression to invoke the constructor with an argument cast to the correct type 288 | NewExpression create = Expression.New(constructor, Expression.Convert(arg, type)); 289 | 290 | // compiles expression into an action delegate 291 | action = Expression.Lambda>(create, arg).Compile(); 292 | _cache.Add(type, action); 293 | } 294 | } 295 | return action; 296 | } 297 | } 298 | 299 | [FieldOffset(0)] 300 | private readonly decimal _decimal; 301 | 302 | // This is actually a VarEnum value, but the VarEnum type requires 4 bytes instead of the expected 2. 303 | [FieldOffset(0)] 304 | private ushort _valueType; 305 | 306 | // Reserved Fields 307 | //[FieldOffset(2)] 308 | //ushort _wReserved1; 309 | //[FieldOffset(4)] 310 | //ushort _wReserved2; 311 | //[FieldOffset(6)] 312 | //ushort _wReserved3; 313 | 314 | [FieldOffset(8)] 315 | private readonly Blob _blob; 316 | 317 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] 318 | [FieldOffset(8)] 319 | private IntPtr _ptr; 320 | 321 | [FieldOffset(8)] 322 | private readonly int _int32; 323 | 324 | [FieldOffset(8)] 325 | private readonly uint _uint32; 326 | 327 | [FieldOffset(8)] 328 | private readonly byte _byte; 329 | 330 | [FieldOffset(8)] 331 | private readonly sbyte _sbyte; 332 | 333 | [FieldOffset(8)] 334 | private readonly short _short; 335 | 336 | [FieldOffset(8)] 337 | private readonly ushort _ushort; 338 | 339 | [FieldOffset(8)] 340 | private readonly long _long; 341 | 342 | [FieldOffset(8)] 343 | private readonly ulong _ulong; 344 | 345 | [FieldOffset(8)] 346 | private readonly double _double; 347 | 348 | [FieldOffset(8)] 349 | private readonly float _float; 350 | 351 | /// Default constrcutor 352 | public PropVar() 353 | { 354 | // left empty 355 | } 356 | 357 | /// Set a string value 358 | public PropVar(string value) 359 | { 360 | if (value == null) 361 | { 362 | throw new ArgumentException("String argument cannot be null or empty.", "value"); 363 | } 364 | 365 | _valueType = (ushort)VarEnum.VT_LPWSTR; 366 | _ptr = Marshal.StringToCoTaskMemUni(value); 367 | } 368 | 369 | /// Set a string vector 370 | public PropVar(string[] value) 371 | { 372 | if (value == null) { throw new ArgumentNullException("value"); } 373 | 374 | InitPropVariantFromStringVector(value, (uint)value.Length, this); 375 | } 376 | 377 | /// Set a bool vector 378 | public PropVar(bool[] value) 379 | { 380 | if (value == null) { throw new ArgumentNullException("value"); } 381 | 382 | InitPropVariantFromBooleanVector(value, (uint)value.Length, this); 383 | } 384 | 385 | /// Set a short vector 386 | public PropVar(short[] value) 387 | { 388 | if (value == null) { throw new ArgumentNullException("value"); } 389 | 390 | InitPropVariantFromInt16Vector(value, (uint)value.Length, this); 391 | } 392 | 393 | /// Set a short vector 394 | public PropVar(ushort[] value) 395 | { 396 | if (value == null) { throw new ArgumentNullException("value"); } 397 | 398 | InitPropVariantFromUInt16Vector(value, (uint)value.Length, this); 399 | } 400 | 401 | /// Set an int vector 402 | public PropVar(int[] value) 403 | { 404 | if (value == null) { throw new ArgumentNullException("value"); } 405 | 406 | InitPropVariantFromInt32Vector(value, (uint)value.Length, this); 407 | } 408 | 409 | /// Set an uint vector 410 | public PropVar(uint[] value) 411 | { 412 | if (value == null) { throw new ArgumentNullException("value"); } 413 | 414 | InitPropVariantFromUInt32Vector(value, (uint)value.Length, this); 415 | } 416 | 417 | /// Set a long vector 418 | public PropVar(long[] value) 419 | { 420 | if (value == null) { throw new ArgumentNullException("value"); } 421 | 422 | InitPropVariantFromInt64Vector(value, (uint)value.Length, this); 423 | } 424 | 425 | /// Set a ulong vector 426 | public PropVar(ulong[] value) 427 | { 428 | if (value == null) { throw new ArgumentNullException("value"); } 429 | 430 | InitPropVariantFromUInt64Vector(value, (uint)value.Length, this); 431 | } 432 | 433 | /// > Set a double vector 434 | public PropVar(double[] value) 435 | { 436 | if (value == null) { throw new ArgumentNullException("value"); } 437 | 438 | InitPropVariantFromDoubleVector(value, (uint)value.Length, this); 439 | } 440 | 441 | /// Set a DateTime vector 442 | public PropVar(DateTime[] value) 443 | { 444 | if (value == null) { throw new ArgumentNullException("value"); } 445 | System.Runtime.InteropServices.ComTypes.FILETIME[] fileTimeArr = 446 | new System.Runtime.InteropServices.ComTypes.FILETIME[value.Length]; 447 | 448 | for (int i = 0; i < value.Length; i++) 449 | { 450 | fileTimeArr[i] = DateTimeToFileTime(value[i]); 451 | } 452 | 453 | InitPropVariantFromFileTimeVector(fileTimeArr, (uint)fileTimeArr.Length, this); 454 | } 455 | 456 | /// Set a bool value 457 | public PropVar(bool value) 458 | { 459 | _valueType = (ushort)VarEnum.VT_BOOL; 460 | _int32 = (value == true) ? -1 : 0; 461 | } 462 | 463 | /// Set a DateTime value 464 | public PropVar(DateTime value) 465 | { 466 | _valueType = (ushort)VarEnum.VT_FILETIME; 467 | 468 | System.Runtime.InteropServices.ComTypes.FILETIME ft = DateTimeToFileTime(value); 469 | InitPropVariantFromFileTime(ref ft, this); 470 | } 471 | 472 | /// Set a byte value 473 | public PropVar(byte value) 474 | { 475 | _valueType = (ushort)VarEnum.VT_UI1; 476 | _byte = value; 477 | } 478 | 479 | /// Set a sbyte value 480 | public PropVar(sbyte value) 481 | { 482 | _valueType = (ushort)VarEnum.VT_I1; 483 | _sbyte = value; 484 | } 485 | 486 | /// Set a short value 487 | public PropVar(short value) 488 | { 489 | _valueType = (ushort)VarEnum.VT_I2; 490 | _short = value; 491 | } 492 | 493 | /// Set an unsigned short value 494 | public PropVar(ushort value) 495 | { 496 | _valueType = (ushort)VarEnum.VT_UI2; 497 | _ushort = value; 498 | } 499 | 500 | /// Set an int value 501 | public PropVar(int value) 502 | { 503 | _valueType = (ushort)VarEnum.VT_I4; 504 | _int32 = value; 505 | } 506 | 507 | /// Set an unsigned int value 508 | public PropVar(uint value) 509 | { 510 | _valueType = (ushort)VarEnum.VT_UI4; 511 | _uint32 = value; 512 | } 513 | 514 | /// Set a decimal value 515 | public PropVar(decimal value) 516 | { 517 | _decimal = value; 518 | 519 | // It is critical that the value type be set after the decimal value, because they overlap. If valuetype is written first, its 520 | // value will be lost when _decimal is written. 521 | _valueType = (ushort)VarEnum.VT_DECIMAL; 522 | } 523 | 524 | /// Create a PropVar with a contained decimal array. 525 | /// Decimal array to wrap. 526 | public PropVar(decimal[] value) 527 | { 528 | if (value == null) { throw new ArgumentNullException("value"); } 529 | 530 | _valueType = (ushort)(VarEnum.VT_DECIMAL | VarEnum.VT_VECTOR); 531 | _int32 = value.Length; 532 | 533 | // allocate required memory for array with 128bit elements 534 | _blob.Pointer = Marshal.AllocCoTaskMem(value.Length * sizeof(decimal)); 535 | for (int i = 0; i < value.Length; i++) 536 | { 537 | int[] bits = decimal.GetBits(value[i]); 538 | Marshal.Copy(bits, 0, _blob.Pointer, bits.Length); 539 | } 540 | } 541 | 542 | /// Create a PropVar containing a float type. 543 | public PropVar(float value) 544 | { 545 | _valueType = (ushort)VarEnum.VT_R4; 546 | 547 | _float = value; 548 | } 549 | 550 | /// Creates a PropVar containing a float[] array. 551 | public PropVar(float[] value) 552 | { 553 | if (value == null) { throw new ArgumentNullException("value"); } 554 | 555 | _valueType = (ushort)(VarEnum.VT_R4 | VarEnum.VT_VECTOR); 556 | _int32 = value.Length; 557 | 558 | _blob.Pointer = Marshal.AllocCoTaskMem(value.Length * sizeof(float)); 559 | 560 | Marshal.Copy(value, 0, _blob.Pointer, value.Length); 561 | } 562 | 563 | /// Set a long 564 | public PropVar(long value) 565 | { 566 | _long = value; 567 | _valueType = (ushort)VarEnum.VT_I8; 568 | } 569 | 570 | /// Set a ulong 571 | public PropVar(ulong value) 572 | { 573 | _valueType = (ushort)VarEnum.VT_UI8; 574 | _ulong = value; 575 | } 576 | 577 | /// Set a double 578 | public PropVar(double value) 579 | { 580 | _valueType = (ushort)VarEnum.VT_R8; 581 | _double = value; 582 | } 583 | 584 | /// Set an IUnknown value 585 | /// The new value to set. 586 | internal void SetIUnknown(object value) 587 | { 588 | _valueType = (ushort)VarEnum.VT_UNKNOWN; 589 | _ptr = Marshal.GetIUnknownForObject(value); 590 | } 591 | 592 | /// Set a safe array value 593 | /// The new value to set. 594 | internal void SetSafeArray(Array array) 595 | { 596 | if (array == null) { throw new ArgumentNullException("array"); } 597 | const ushort vtUnknown = 13; 598 | IntPtr psa = SafeArrayCreateVector(vtUnknown, 0, (uint)array.Length); 599 | 600 | IntPtr pvData = SafeArrayAccessData(psa); 601 | try // to remember to release lock on data 602 | { 603 | for (int i = 0; i < array.Length; ++i) 604 | { 605 | object obj = array.GetValue(i); 606 | IntPtr punk = (obj != null) ? Marshal.GetIUnknownForObject(obj) : IntPtr.Zero; 607 | Marshal.WriteIntPtr(pvData, i * IntPtr.Size, punk); 608 | } 609 | } 610 | finally 611 | { 612 | SafeArrayUnaccessData(psa); 613 | } 614 | 615 | _valueType = (ushort)VarEnum.VT_ARRAY | (ushort)VarEnum.VT_UNKNOWN; 616 | _ptr = psa; 617 | } 618 | 619 | /// Gets or sets the variant type. 620 | public VarEnum VarType 621 | { 622 | get => (VarEnum)_valueType; 623 | set => _valueType = (ushort)value; 624 | } 625 | 626 | /// Checks if this has an empty or null value 627 | /// 628 | public bool IsNullOrEmpty => (_valueType == (ushort)VarEnum.VT_EMPTY || _valueType == (ushort)VarEnum.VT_NULL); 629 | 630 | /// Gets the variant value. 631 | public object Value 632 | { 633 | get 634 | { 635 | switch ((VarEnum)_valueType) 636 | { 637 | case VarEnum.VT_I1: 638 | return _sbyte; 639 | 640 | case VarEnum.VT_UI1: 641 | return _byte; 642 | 643 | case VarEnum.VT_I2: 644 | return _short; 645 | 646 | case VarEnum.VT_UI2: 647 | return _ushort; 648 | 649 | case VarEnum.VT_I4: 650 | case VarEnum.VT_INT: 651 | return _int32; 652 | 653 | case VarEnum.VT_UI4: 654 | case VarEnum.VT_UINT: 655 | return _uint32; 656 | 657 | case VarEnum.VT_I8: 658 | return _long; 659 | 660 | case VarEnum.VT_UI8: 661 | return _ulong; 662 | 663 | case VarEnum.VT_R4: 664 | return _float; 665 | 666 | case VarEnum.VT_R8: 667 | return _double; 668 | 669 | case VarEnum.VT_BOOL: 670 | return _int32 == -1; 671 | 672 | case VarEnum.VT_ERROR: 673 | return _long; 674 | 675 | case VarEnum.VT_CY: 676 | return _decimal; 677 | 678 | case VarEnum.VT_DATE: 679 | return DateTime.FromOADate(_double); 680 | 681 | case VarEnum.VT_FILETIME: 682 | return DateTime.FromFileTime(_long); 683 | 684 | case VarEnum.VT_BSTR: 685 | return Marshal.PtrToStringBSTR(_ptr); 686 | 687 | case VarEnum.VT_BLOB: 688 | return GetBlobData(); 689 | 690 | case VarEnum.VT_LPSTR: 691 | return Marshal.PtrToStringAnsi(_ptr); 692 | 693 | case VarEnum.VT_LPWSTR: 694 | return Marshal.PtrToStringUni(_ptr); 695 | 696 | case VarEnum.VT_UNKNOWN: 697 | return Marshal.GetObjectForIUnknown(_ptr); 698 | 699 | case VarEnum.VT_DISPATCH: 700 | return Marshal.GetObjectForIUnknown(_ptr); 701 | 702 | case VarEnum.VT_DECIMAL: 703 | return _decimal; 704 | 705 | case VarEnum.VT_ARRAY | VarEnum.VT_UNKNOWN: 706 | return CrackSingleDimSafeArray(_ptr); 707 | 708 | case (VarEnum.VT_VECTOR | VarEnum.VT_LPWSTR): 709 | return GetVector(); 710 | 711 | case (VarEnum.VT_VECTOR | VarEnum.VT_I2): 712 | return GetVector(); 713 | 714 | case (VarEnum.VT_VECTOR | VarEnum.VT_UI2): 715 | return GetVector(); 716 | 717 | case (VarEnum.VT_VECTOR | VarEnum.VT_I4): 718 | return GetVector(); 719 | 720 | case (VarEnum.VT_VECTOR | VarEnum.VT_UI4): 721 | return GetVector(); 722 | 723 | case (VarEnum.VT_VECTOR | VarEnum.VT_I8): 724 | return GetVector(); 725 | 726 | case (VarEnum.VT_VECTOR | VarEnum.VT_UI8): 727 | return GetVector(); 728 | 729 | case (VarEnum.VT_VECTOR | VarEnum.VT_R4): 730 | return GetVector(); 731 | 732 | case (VarEnum.VT_VECTOR | VarEnum.VT_R8): 733 | return GetVector(); 734 | 735 | case (VarEnum.VT_VECTOR | VarEnum.VT_BOOL): 736 | return GetVector(); 737 | 738 | case (VarEnum.VT_VECTOR | VarEnum.VT_FILETIME): 739 | return GetVector(); 740 | 741 | case (VarEnum.VT_VECTOR | VarEnum.VT_DECIMAL): 742 | return GetVector(); 743 | 744 | default: 745 | // if the value cannot be marshaled 746 | return null; 747 | } 748 | } 749 | } 750 | 751 | private static long GetFileTimeAsLong(ref System.Runtime.InteropServices.ComTypes.FILETIME val) => (((long)val.dwHighDateTime) << 32) + val.dwLowDateTime; 752 | 753 | private static System.Runtime.InteropServices.ComTypes.FILETIME DateTimeToFileTime(DateTime value) 754 | { 755 | long hFT = value.ToFileTime(); 756 | System.Runtime.InteropServices.ComTypes.FILETIME ft = 757 | new System.Runtime.InteropServices.ComTypes.FILETIME 758 | { 759 | dwLowDateTime = (int)(hFT & 0xFFFFFFFF), 760 | dwHighDateTime = (int)(hFT >> 32) 761 | }; 762 | return ft; 763 | } 764 | 765 | private object GetBlobData() 766 | { 767 | byte[] blobData = new byte[_int32]; 768 | 769 | IntPtr pBlobData = _blob.Pointer; 770 | Marshal.Copy(pBlobData, blobData, 0, _int32); 771 | 772 | return blobData; 773 | } 774 | 775 | private Array GetVector() 776 | { 777 | int count = PropVariantGetElementCount(this); 778 | if (count <= 0) { return null; } 779 | 780 | lock (_padlock) 781 | { 782 | if (_vectorActions == null) 783 | { 784 | _vectorActions = GenerateVectorActions(); 785 | } 786 | } 787 | 788 | if (!_vectorActions.TryGetValue(typeof(T), out var action)) 789 | { 790 | throw new InvalidCastException("Cannot be cast to unsupported type."); 791 | } 792 | 793 | Array array = new T[count]; 794 | for (uint i = 0; i < count; i++) 795 | { 796 | action(this, array, i); 797 | } 798 | 799 | return array; 800 | } 801 | 802 | private static Array CrackSingleDimSafeArray(IntPtr psa) 803 | { 804 | uint cDims = SafeArrayGetDim(psa); 805 | if (cDims != 1) 806 | throw new ArgumentException("Multi-dimensional SafeArrays not supported.", "psa"); 807 | 808 | int lBound = SafeArrayGetLBound(psa, 1U); 809 | int uBound = SafeArrayGetUBound(psa, 1U); 810 | 811 | int n = uBound - lBound + 1; // uBound is inclusive 812 | 813 | object[] array = new object[n]; 814 | for (int i = lBound; i <= uBound; ++i) 815 | { 816 | array[i] = SafeArrayGetElement(psa, ref i); 817 | } 818 | 819 | return array; 820 | } 821 | 822 | /// Disposes the object, calls the clear function. 823 | public void Dispose() 824 | { 825 | PropVariantClear(this); 826 | 827 | GC.SuppressFinalize(this); 828 | } 829 | 830 | /// Finalizer 831 | ~PropVar() 832 | { 833 | Dispose(); 834 | } 835 | 836 | /// Provides an simple string representation of the contained data and type. 837 | /// 838 | public override string ToString() => string.Format(System.Globalization.CultureInfo.InvariantCulture, 839 | "{0}: {1}", Value, VarType.ToString()); 840 | } 841 | } -------------------------------------------------------------------------------- /intag/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("intag")] 5 | [assembly: AssemblyDescription("InTag")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("Jamminroot")] 8 | [assembly: AssemblyProduct("intag")] 9 | [assembly: AssemblyCopyright("Copyright © Jamminroot 2023")] 10 | [assembly: AssemblyTrademark("InTag")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("60bca8d2-ff4b-4d4a-8ca8-4d1ff7d21cd6")] 16 | [assembly: AssemblyVersion("1.6.0.0")] 17 | [assembly: AssemblyFileVersion("1.6.0.0")] -------------------------------------------------------------------------------- /intag/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | 10 | namespace intag.Properties { 11 | using System; 12 | 13 | 14 | /// 15 | /// A strongly-typed resource class, for looking up localized strings, etc. 16 | /// 17 | // This class was auto-generated by the StronglyTypedResourceBuilder 18 | // class via a tool like ResGen or Visual Studio. 19 | // To add or remove a member, edit your .ResX file then rerun ResGen 20 | // with the /str option, or rebuild your VS project. 21 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 22 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 23 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 24 | internal class Resources { 25 | 26 | private static global::System.Resources.ResourceManager resourceMan; 27 | 28 | private static global::System.Globalization.CultureInfo resourceCulture; 29 | 30 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 31 | internal Resources() { 32 | } 33 | 34 | /// 35 | /// Returns the cached ResourceManager instance used by this class. 36 | /// 37 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 38 | internal static global::System.Resources.ResourceManager ResourceManager { 39 | get { 40 | if (object.ReferenceEquals(resourceMan, null)) { 41 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("intag.Properties.Resources", typeof(Resources).Assembly); 42 | resourceMan = temp; 43 | } 44 | return resourceMan; 45 | } 46 | } 47 | 48 | /// 49 | /// Overrides the current thread's CurrentUICulture property for all 50 | /// resource lookups using this strongly typed resource class. 51 | /// 52 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 53 | internal static global::System.Globalization.CultureInfo Culture { 54 | get { 55 | return resourceCulture; 56 | } 57 | set { 58 | resourceCulture = value; 59 | } 60 | } 61 | 62 | /// 63 | /// Looks up a localized resource of type System.Drawing.Bitmap. 64 | /// 65 | internal static System.Drawing.Bitmap add { 66 | get { 67 | object obj = ResourceManager.GetObject("add", resourceCulture); 68 | return ((System.Drawing.Bitmap)(obj)); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized resource of type System.Drawing.Bitmap. 74 | /// 75 | internal static System.Drawing.Bitmap remove { 76 | get { 77 | object obj = ResourceManager.GetObject("remove", resourceCulture); 78 | return ((System.Drawing.Bitmap)(obj)); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /intag/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | text/microsoft-resx 51 | 52 | 53 | 2.0 54 | 55 | 56 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 57 | 58 | 59 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 60 | 61 | 62 | 63 | ..\Resources\add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 64 | 65 | 66 | ..\Resources\remove.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 67 | 68 | -------------------------------------------------------------------------------- /intag/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace intag.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /intag/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /intag/Properties/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | true 43 | true 44 | 45 | 46 | 47 | 48 | 62 | -------------------------------------------------------------------------------- /intag/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "intag": { 4 | "commandName": "Project", 5 | "commandLineArgs": "C:\\temp\\test1" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /intag/PropertyKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace intag 5 | { 6 | [StructLayout(LayoutKind.Sequential, Pack = 4)] 7 | public struct PropertyKey : IEquatable 8 | { 9 | private Guid formatId; 10 | private readonly int propertyId; 11 | 12 | /// A unique GUID for the property 13 | public Guid FormatId => formatId; 14 | 15 | /// Property identifier (PID) 16 | public int PropertyId => propertyId; 17 | 18 | /// PropertyKey Constructor 19 | /// A unique GUID for the property 20 | /// Property identifier (PID) 21 | public PropertyKey(Guid formatId, int propertyId) 22 | { 23 | this.formatId = formatId; 24 | this.propertyId = propertyId; 25 | } 26 | 27 | /// PropertyKey Constructor 28 | /// A string represenstion of a GUID for the property 29 | /// Property identifier (PID) 30 | public PropertyKey(string formatId, int propertyId) 31 | { 32 | this.formatId = new Guid(formatId); 33 | this.propertyId = propertyId; 34 | } 35 | 36 | /// Returns whether this object is equal to another. This is vital for performance of value types. 37 | /// The object to compare against. 38 | /// Equality result. 39 | public bool Equals(PropertyKey other) => other.Equals((object)this); 40 | 41 | /// Returns the hash code of the object. This is vital for performance of value types. 42 | /// 43 | public override int GetHashCode() => formatId.GetHashCode() ^ propertyId; 44 | 45 | /// Returns whether this object is equal to another. This is vital for performance of value types. 46 | /// The object to compare against. 47 | /// Equality result. 48 | public override bool Equals(object obj) 49 | { 50 | if (obj == null) 51 | return false; 52 | 53 | if (!(obj is PropertyKey)) 54 | return false; 55 | 56 | var other = (PropertyKey)obj; 57 | return other.formatId.Equals(formatId) && (other.propertyId == propertyId); 58 | } 59 | 60 | /// Implements the == (equality) operator. 61 | /// First property key to compare. 62 | /// Second property key to compare. 63 | /// true if object a equals object b. false otherwise. 64 | public static bool operator ==(PropertyKey propertyKey1, PropertyKey propertyKey2) => propertyKey1.Equals(propertyKey2); 65 | 66 | /// Implements the != (inequality) operator. 67 | /// First property key to compare 68 | /// Second property key to compare. 69 | /// true if object a does not equal object b. false otherwise. 70 | public static bool operator !=(PropertyKey propertyKey1, PropertyKey propertyKey2) => !propertyKey1.Equals(propertyKey2); 71 | 72 | /// Override ToString() to provide a user friendly string representation 73 | /// String representing the property key 74 | public override string ToString() => string.Format(System.Globalization.CultureInfo.InvariantCulture, 75 | "{0}, {1}", 76 | formatId.ToString("B"), propertyId); 77 | } 78 | } -------------------------------------------------------------------------------- /intag/RegUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Windows.Forms; 4 | using Microsoft.Win32; 5 | 6 | namespace intag 7 | { 8 | public static class RegUtils 9 | { 10 | private static void InstallExtension(string ext) 11 | { 12 | var subKey = Registry.ClassesRoot.CreateSubKey($@"{ext}\shell\{Constants.RegistryName}\command") 13 | ?? throw new ApplicationException("Cannot create shell registry key"); 14 | var subKeyIcon = Registry.ClassesRoot.OpenSubKey($@"{ext}\shell\{Constants.RegistryName}", true) 15 | ?? throw new ApplicationException("Cannot open shell registry key"); 16 | 17 | var loc = Application.ExecutablePath; 18 | if (loc == null) 19 | { 20 | subKey.Close(); 21 | subKeyIcon.Close(); 22 | 23 | throw new ApplicationException("Cannot get assembly's location"); 24 | } 25 | 26 | loc = $"\"{loc}\""; 27 | 28 | subKeyIcon.SetValue("Icon", loc); 29 | subKeyIcon.SetValue("MultiSelectModel", "Player"); 30 | subKeyIcon.Close(); 31 | subKey.SetValue("", loc+" --ui --path \"%1\""); 32 | subKey.Close(); 33 | } 34 | 35 | private static bool TryGetSystemColor(string colorName, out System.Drawing.Color color) 36 | { 37 | color = System.Drawing.Color.Empty; 38 | var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\DWM"); 39 | if (key == null) 40 | { 41 | return false; 42 | } 43 | 44 | color = System.Drawing.Color.FromArgb((int)key.GetValue(colorName)); 45 | return true; 46 | } 47 | 48 | public static bool TryGetSystemAccentColor(out System.Drawing.Color color) 49 | { 50 | return TryGetSystemColor("AccentColor", out color); 51 | } 52 | 53 | public static bool TryGetSystemColorizationColor(out System.Drawing.Color color) 54 | { 55 | return TryGetSystemColor("ColorizationColor", out color); 56 | } 57 | 58 | private static void InstallFiles() => InstallExtension("*"); 59 | private static void InstallFolder() => InstallExtension("Folder"); 60 | 61 | public static void Install() 62 | { 63 | InstallFolder(); 64 | InstallFiles(); 65 | } 66 | 67 | private static void UninstallExt(string ext) 68 | { 69 | Registry.ClassesRoot.DeleteSubKeyTree($@"{ext}\shell\{Constants.RegistryName}", false); 70 | } 71 | 72 | private static void UninstallFolder() => UninstallExt("Folder"); 73 | private static void UninstallFiles() => UninstallExt("*"); 74 | 75 | public static void Uninstall() 76 | { 77 | UninstallFolder(); 78 | UninstallFiles(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /intag/Resources/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/intag/Resources/add.png -------------------------------------------------------------------------------- /intag/Resources/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/intag/Resources/remove.png -------------------------------------------------------------------------------- /intag/ShEnums.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace intag 4 | { 5 | internal enum ShellItemDesignNameOptions 6 | { 7 | Normal = 0x00000000, // SIGDN_NORMAL 8 | ParentRelativeParsing = unchecked((int)0x80018001), // SIGDN_INFOLDER | SIGDN_FORPARSING 9 | DesktopAbsoluteParsing = unchecked((int)0x80028000), // SIGDN_FORPARSING 10 | ParentRelativeEditing = unchecked((int)0x80031001), // SIGDN_INFOLDER | SIGDN_FOREDITING 11 | DesktopAbsoluteEditing = unchecked((int)0x8004c000), // SIGDN_FORPARSING | SIGDN_FORADDRESSBAR 12 | FileSystemPath = unchecked((int)0x80058000), // SIGDN_FORPARSING 13 | Url = unchecked((int)0x80068000), // SIGDN_FORPARSING 14 | ParentRelativeForAddressBar = unchecked((int)0x8007c001), // SIGDN_INFOLDER | SIGDN_FORPARSING | SIGDN_FORADDRESSBAR 15 | ParentRelative = unchecked((int)0x80080001) // SIGDN_INFOLDER 16 | } 17 | 18 | [Flags] 19 | internal enum ShellFileGetAttributesOptions 20 | { 21 | /// The specified items can be copied. 22 | CanCopy = 0x00000001, 23 | 24 | /// The specified items can be moved. 25 | CanMove = 0x00000002, 26 | 27 | /// 28 | /// Shortcuts can be created for the specified items. This flag has the same value as DROPEFFECT. The normal use of this flag is 29 | /// to add a Create Shortcut item to the shortcut menu that is displayed during drag-and-drop operations. However, SFGAO_CANLINK 30 | /// also adds a Create Shortcut item to the Microsoft Windows Explorer's File menu and to normal shortcut menus. If this item is 31 | /// selected, your application's IContextMenu::InvokeCommand is invoked with the lpVerb member of the CMINVOKECOMMANDINFO 32 | /// structure set to "link." Your application is responsible for creating the link. 33 | /// 34 | CanLink = 0x00000004, 35 | 36 | /// The specified items can be bound to an IStorage interface through IShellFolder::BindToObject. 37 | Storage = 0x00000008, 38 | 39 | /// The specified items can be renamed. 40 | CanRename = 0x00000010, 41 | 42 | /// The specified items can be deleted. 43 | CanDelete = 0x00000020, 44 | 45 | /// The specified items have property sheets. 46 | HasPropertySheet = 0x00000040, 47 | 48 | /// The specified items are drop targets. 49 | DropTarget = 0x00000100, 50 | 51 | /// This flag is a mask for the capability flags. 52 | CapabilityMask = 0x00000177, 53 | 54 | /// Windows 7 and later. The specified items are system items. 55 | System = 0x00001000, 56 | 57 | /// The specified items are encrypted. 58 | Encrypted = 0x00002000, 59 | 60 | /// 61 | /// Indicates that accessing the object = through IStream or other storage interfaces, is a slow operation. Applications should 62 | /// avoid accessing items flagged with SFGAO_ISSLOW. 63 | /// 64 | IsSlow = 0x00004000, 65 | 66 | /// The specified items are ghosted icons. 67 | Ghosted = 0x00008000, 68 | 69 | /// The specified items are shortcuts. 70 | Link = 0x00010000, 71 | 72 | /// The specified folder objects are shared. 73 | Share = 0x00020000, 74 | 75 | /// 76 | /// The specified items are read-only. In the case of folders, this means that new items cannot be created in those folders. 77 | /// 78 | ReadOnly = 0x00040000, 79 | 80 | /// 81 | /// The item is hidden and should not be displayed unless the Show hidden files and folders option is enabled in Folder Settings. 82 | /// 83 | Hidden = 0x00080000, 84 | 85 | /// This flag is a mask for the display attributes. 86 | DisplayAttributeMask = 0x000FC000, 87 | 88 | /// The specified folders contain one or more file system folders. 89 | FileSystemAncestor = 0x10000000, 90 | 91 | /// The specified items are folders. 92 | Folder = 0x20000000, 93 | 94 | /// 95 | /// The specified folders or file objects are part of the file system that is, they are files, directories, or root directories). 96 | /// 97 | FileSystem = 0x40000000, 98 | 99 | /// The specified folders have subfolders = and are, therefore, expandable in the left pane of Windows Explorer). 100 | HasSubFolder = unchecked((int)0x80000000), 101 | 102 | /// This flag is a mask for the contents attributes. 103 | ContentsMask = unchecked((int)0x80000000), 104 | 105 | /// 106 | /// When specified as input, SFGAO_VALIDATE instructs the folder to validate that the items pointed to by the contents of apidl 107 | /// exist. If one or more of those items do not exist, IShellFolder::GetAttributesOf returns a failure code. When used with the 108 | /// file system folder, SFGAO_VALIDATE instructs the folder to discard cached properties retrieved by clients of 109 | /// IShellFolder2::GetDetailsEx that may have accumulated for the specified items. 110 | /// 111 | Validate = 0x01000000, 112 | 113 | /// The specified items are on removable media or are themselves removable devices. 114 | Removable = 0x02000000, 115 | 116 | /// The specified items are compressed. 117 | Compressed = 0x04000000, 118 | 119 | /// The specified items can be browsed in place. 120 | Browsable = 0x08000000, 121 | 122 | /// The items are nonenumerated items. 123 | Nonenumerated = 0x00100000, 124 | 125 | /// The objects contain new content. 126 | NewContent = 0x00200000, 127 | 128 | /// It is possible to create monikers for the specified file objects or folders. 129 | CanMoniker = 0x00400000, 130 | 131 | /// Not supported. 132 | HasStorage = 0x00400000, 133 | 134 | /// 135 | /// Indicates that the item has a stream associated with it that can be accessed by a call to IShellFolder::BindToObject with 136 | /// IID_IStream in the riid parameter. 137 | /// 138 | Stream = 0x00400000, 139 | 140 | /// 141 | /// Children of this item are accessible through IStream or IStorage. Those children are flagged with SFGAO_STORAGE or SFGAO_STREAM. 142 | /// 143 | StorageAncestor = 0x00800000, 144 | 145 | /// This flag is a mask for the storage capability attributes. 146 | StorageCapabilityMask = 0x70C50008, 147 | 148 | /// 149 | /// Mask used by PKEY_SFGAOFlags to remove certain values that are considered to cause slow calculations or lack context. Equal 150 | /// to SFGAO_VALIDATE | SFGAO_ISSLOW | SFGAO_HASSUBFOLDER. 151 | /// 152 | PkeyMask = unchecked((int)0x81044000), 153 | } 154 | 155 | /// 156 | /// Indicate flags that modify the property store object retrieved by methods that create a property store, such as 157 | /// IShellItem2::GetPropertyStore or IPropertyStoreFactory::GetPropertyStore. 158 | /// 159 | [Flags] 160 | internal enum GetPropertyStoreOptions 161 | { 162 | /// 163 | /// Meaning to a calling process: Return a read-only property store that contains all properties. Slow items (offline files) are 164 | /// not opened. Combination with other flags: Can be overridden by other flags. 165 | /// 166 | Default = 0, 167 | 168 | /// 169 | /// Meaning to a calling process: Include only properties directly from the property handler, which opens the file on the disk, 170 | /// network, or device. Meaning to a file 171 | /// folder: Only include properties directly from the handler. 172 | /// 173 | /// Meaning to other folders: When delegating to a file folder, pass this flag on to the file folder; do not do any multiplexing 174 | /// (MUX). When not delegating to a file folder, ignore this flag instead of returning a failure code. 175 | /// 176 | /// Combination with other flags: Cannot be combined with GPS_TEMPORARY, GPS_FASTPROPERTIESONLY, or GPS_BESTEFFORT. 177 | /// 178 | HandlePropertiesOnly = 0x1, 179 | 180 | /// 181 | /// Meaning to a calling process: Can write properties to the item. 182 | /// Note: The store may contain fewer properties than a read-only store. 183 | /// 184 | /// Meaning to a file folder: ReadWrite. 185 | /// 186 | /// Meaning to other folders: ReadWrite. Note: When using default MUX, return a single unmultiplexed store because the default 187 | /// MUX does not support ReadWrite. 188 | /// 189 | /// Combination with other flags: Cannot be combined with GPS_TEMPORARY, GPS_FASTPROPERTIESONLY, GPS_BESTEFFORT, or 190 | /// GPS_DELAYCREATION. Implies GPS_HANDLERPROPERTIESONLY. 191 | /// 192 | ReadWrite = 0x2, 193 | 194 | /// 195 | /// Meaning to a calling process: Provides a writable store, with no initial properties, that exists for the lifetime of the 196 | /// Shell item instance; basically, a property bag attached to the item instance. 197 | /// 198 | /// Meaning to a file folder: Not applicable. Handled by the Shell item. 199 | /// 200 | /// Meaning to other folders: Not applicable. Handled by the Shell item. 201 | /// 202 | /// Combination with other flags: Cannot be combined with any other flag. Implies GPS_READWRITE 203 | /// 204 | Temporary = 0x4, 205 | 206 | /// 207 | /// Meaning to a calling process: Provides a store that does not involve reading from the disk or network. Note: Some values may 208 | /// be different, or missing, compared to a store without this flag. 209 | /// 210 | /// Meaning to a file folder: Include the "innate" and "fallback" stores only. Do not load the handler. 211 | /// 212 | /// Meaning to other folders: Include only properties that are available in memory or can be computed very quickly (no properties 213 | /// from disk, network, or peripheral IO devices). This is normally only data sources from the IDLIST. When delegating to other 214 | /// folders, pass this flag on to them. 215 | /// 216 | /// Combination with other flags: Cannot be combined with GPS_TEMPORARY, GPS_READWRITE, GPS_HANDLERPROPERTIESONLY, or GPS_DELAYCREATION. 217 | /// 218 | FastPropertiesOnly = 0x8, 219 | 220 | /// 221 | /// Meaning to a calling process: Open a slow item (offline file) if necessary. Meaning to a file folder: Retrieve a file from 222 | /// offline storage, if necessary. 223 | /// Note: Without this flag, the handler is not created for offline files. 224 | /// 225 | /// Meaning to other folders: Do not return any properties that are very slow. 226 | /// 227 | /// Combination with other flags: Cannot be combined with GPS_TEMPORARY or GPS_FASTPROPERTIESONLY. 228 | /// 229 | OpensLowItem = 0x10, 230 | 231 | /// 232 | /// Meaning to a calling process: Delay memory-intensive operations, such as file access, until a property is requested that 233 | /// requires such access. 234 | /// 235 | /// Meaning to a file folder: Do not create the handler until needed; for example, either GetCount/GetAt or GetValue, where the 236 | /// innate store does not satisfy the request. 237 | /// Note: GetValue might fail due to file access problems. 238 | /// 239 | /// Meaning to other folders: If the folder has memory-intensive properties, such as delegating to a file folder or network 240 | /// access, it can optimize performance by supporting IDelayedPropertyStoreFactory and splitting up its properties into a fast 241 | /// and a slow store. It can then use delayed MUX to recombine them. 242 | /// 243 | /// Combination with other flags: Cannot be combined with GPS_TEMPORARY or GPS_READWRITE 244 | /// 245 | DelayCreation = 0x20, 246 | 247 | /// 248 | /// Meaning to a calling process: Succeed at getting the store, even if some properties are not returned. Note: Some values may 249 | /// be different, or missing, compared to a store without this flag. 250 | /// 251 | /// Meaning to a file folder: Succeed and return a store, even if the handler or innate store has an error during creation. Only 252 | /// fail if substores fail. 253 | /// 254 | /// Meaning to other folders: Succeed on getting the store, even if some properties are not returned. 255 | /// 256 | /// Combination with other flags: Cannot be combined with GPS_TEMPORARY, GPS_READWRITE, or GPS_HANDLERPROPERTIESONLY. 257 | /// 258 | BestEffort = 0x40, 259 | 260 | /// Mask for valid GETPROPERTYSTOREFLAGS values. 261 | MaskValid = 0xff, 262 | } 263 | } -------------------------------------------------------------------------------- /intag/System/Windows/Forms/RoundedButton.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Drawing.Drawing2D; 3 | 4 | namespace System.Windows.Forms 5 | { 6 | internal class RoundedButton : Button 7 | { 8 | GraphicsPath GetRoundPath(RectangleF rect, int radius) 9 | { 10 | var r2 = radius / 2f; 11 | var graphPath = new GraphicsPath(); 12 | graphPath.AddArc(rect.X, rect.Y, radius, radius, 180, 90); 13 | graphPath.AddLine(rect.X + r2, rect.Y, rect.Width - r2, rect.Y); 14 | graphPath.AddArc(rect.X + rect.Width - radius, rect.Y, radius, radius, 270, 90); 15 | graphPath.AddLine(rect.Width, rect.Y + r2, rect.Width, rect.Height - r2); 16 | graphPath.AddArc(rect.X + rect.Width - radius, 17 | rect.Y + rect.Height - radius, radius, radius, 0, 90); 18 | graphPath.AddLine(rect.Width - r2, rect.Height, rect.X + r2, rect.Height); 19 | graphPath.AddArc(rect.X, rect.Y + rect.Height - radius, radius, radius, 90, 90); 20 | graphPath.AddLine(rect.X, rect.Height - r2, rect.X, rect.Y + r2); 21 | graphPath.CloseFigure(); 22 | return graphPath; 23 | } 24 | 25 | protected override void OnPaint(PaintEventArgs e) 26 | { 27 | base.OnPaint(e); 28 | var rect = new RectangleF(0, 0, Width, Height); 29 | using (var graphPath = GetRoundPath(rect, 8)) 30 | { 31 | this.Region = new Region(graphPath); 32 | using (var pen = new Pen(Color.FromArgb(255, 231, 143), 1.55f)) 33 | { 34 | pen.Alignment = PenAlignment.Inset; 35 | e.Graphics.DrawPath(pen, graphPath); 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /intag/System/Windows/Forms/RoundedTextBox.cs: -------------------------------------------------------------------------------- 1 | namespace System.Windows.Forms 2 | { 3 | internal class RoundedTextBox : TextBox 4 | { 5 | [System.Runtime.InteropServices.DllImport("gdi32.dll", EntryPoint = "CreateRoundRectRgn")] 6 | private static extern IntPtr CreateRoundRectRgn 7 | ( 8 | int nLeftRect, 9 | int nTopRect, 10 | int nRightRect, 11 | int nBottomRect, 12 | 13 | int nHeightRect, 14 | int nWeightRect 15 | ); 16 | 17 | protected override void OnResize(EventArgs e) 18 | { 19 | base.OnResize(e); 20 | Region = Drawing.Region.FromHrgn(CreateRoundRectRgn(1, 1, Width, Height, 1, 1)); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /intag/WindowUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Runtime.InteropServices; 4 | using System.Windows.Forms; 5 | 6 | namespace intag 7 | { 8 | public static class WindowUtils 9 | { 10 | public static void EnableAcrylic(IWin32Window window, Color blurColor) 11 | { 12 | unsafe 13 | { 14 | var accent = new AccentPolicy 15 | { 16 | AccentState = ACCENT.ENABLE_ACRYLICBLURBEHIND, 17 | GradientColor = ToAbgr(blurColor) 18 | }; 19 | 20 | var accentStructSize = Marshal.SizeOf(accent); 21 | var accentPtr = Marshal.AllocHGlobal(accentStructSize); 22 | Marshal.StructureToPtr(accent, accentPtr, false); 23 | 24 | var data = new WindowCompositionAttributeData 25 | { 26 | Attribute = WCA.ACCENT_POLICY, 27 | Data = (void*)accentPtr, 28 | DataLength = accentStructSize 29 | }; 30 | 31 | SetWindowCompositionAttribute(new HandleRef(null, window.Handle), in data); 32 | 33 | Marshal.FreeHGlobal(accentPtr); 34 | } 35 | } 36 | 37 | private static uint ToAbgr(Color color) 38 | { 39 | var a = color.A; 40 | var r = color.R; 41 | var g = color.G; 42 | var b = color.B; 43 | 44 | return (uint)((a << 24) | (b << 16) | (g << 8) | r); 45 | } 46 | 47 | // ReSharper disable InconsistentNaming, UnusedMember.Local, NotAccessedField.Local 48 | #pragma warning disable 649 49 | 50 | // Discovered via: 51 | // https://withinrafael.com/2015/07/08/adding-the-aero-glass-blur-to-your-windows-10-apps/ 52 | // https://github.com/riverar/sample-win32-acrylicblur/blob/917adc277c7258307799327d12262ebd47fd0308/MainWindow.xaml.cs 53 | 54 | [DllImport("user32.dll")] 55 | private static extern int SetWindowCompositionAttribute(HandleRef hWnd, in WindowCompositionAttributeData data); 56 | 57 | private unsafe struct WindowCompositionAttributeData 58 | { 59 | public WCA Attribute; 60 | public void* Data; 61 | public int DataLength; 62 | } 63 | 64 | private enum WCA 65 | { 66 | ACCENT_POLICY = 19 67 | } 68 | 69 | private enum ACCENT 70 | { 71 | DISABLED = 0, 72 | ENABLE_GRADIENT = 1, 73 | ENABLE_TRANSPARENTGRADIENT = 2, 74 | ENABLE_BLURBEHIND = 3, 75 | ENABLE_ACRYLICBLURBEHIND = 4, 76 | INVALID_STATE = 5 77 | } 78 | 79 | private struct AccentPolicy 80 | { 81 | public ACCENT AccentState; 82 | public uint AccentFlags; 83 | public uint GradientColor; 84 | public uint AnimationId; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /intag/intag - Backup.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net5.0-windows 4 | true 5 | true 6 | WinExe 7 | false 8 | false 9 | 9 10 | publish\ 11 | true 12 | Disk 13 | false 14 | Foreground 15 | 7 16 | Days 17 | false 18 | false 19 | true 20 | 0 21 | 1.6.0.0 22 | true 23 | true 24 | false 25 | true 26 | true 27 | 1.6.0.0 28 | 1.6.0.0 29 | 30 | 31 | ..\bin\Debug\ 32 | true 33 | 34 | 35 | ..\bin\Release\ 36 | GlobalizationRules.ruleset 37 | false 38 | true 39 | 40 | 41 | Internet 42 | 43 | 44 | false 45 | 46 | 47 | false 48 | 49 | 50 | C91A6270149A6516DD4375AEAC01191F4ACAFEB4 51 | 52 | 53 | 54 | 55 | 56 | 57 | intag_512.ico 58 | 59 | 60 | Properties\app.manifest 61 | 62 | 63 | Jamminroot.pfx 64 | 65 | 66 | false 67 | 68 | 69 | intag.Program 70 | 71 | 72 | 73 | Component 74 | 75 | 76 | Component 77 | 78 | 79 | Component 80 | 81 | 82 | 83 | 84 | README.md 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | SettingsSingleFileGenerator 96 | 97 | 98 | -------------------------------------------------------------------------------- /intag/intag.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0-windows 4 | true 5 | true 6 | WinExe 7 | false 8 | false 9 | 9 10 | publish\ 11 | true 12 | Disk 13 | false 14 | Foreground 15 | 7 16 | Days 17 | false 18 | false 19 | true 20 | 0 21 | 1.8.1.0 22 | true 23 | true 24 | false 25 | true 26 | true 27 | 1.8.1.0 28 | 1.8.1.0 29 | 30 | 31 | ..\bin\Debug\ 32 | true 33 | full 34 | 35 | 36 | ..\bin\Release\ 37 | true 38 | 39 | 40 | Internet 41 | 42 | 43 | false 44 | 45 | 46 | false 47 | 48 | 49 | C91A6270149A6516DD4375AEAC01191F4ACAFEB4 50 | 51 | 52 | 53 | 54 | 55 | 56 | intag_512.ico 57 | 58 | 59 | Properties\app.manifest 60 | 61 | 62 | Jamminroot.pfx 63 | 64 | 65 | false 66 | 67 | 68 | intag.Program 69 | 70 | 71 | 72 | Component 73 | 74 | 75 | Component 76 | 77 | 78 | 79 | 80 | README.md 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /intag/intag_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/intag/intag_32.ico -------------------------------------------------------------------------------- /intag/intag_512.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jamminroot/intag/c0e4b6babfa5cff493088e8593030addaabd6846/intag/intag_512.ico -------------------------------------------------------------------------------- /intag/intag_form.Designer.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Drawing; 3 | using System.Windows.Forms.VisualStyles; 4 | 5 | namespace intag 6 | { 7 | partial class MainForm 8 | { 9 | /// 10 | /// Required designer variable. 11 | /// 12 | private System.ComponentModel.IContainer components = null; 13 | 14 | /// 15 | /// Clean up any resources being used. 16 | /// 17 | /// true if managed resources should be disposed; otherwise, false. 18 | protected override void Dispose(bool disposing) 19 | { 20 | if (disposing && (components != null)) 21 | { 22 | components.Dispose(); 23 | } 24 | base.Dispose(disposing); 25 | } 26 | 27 | #region Windows Form Designer generated code 28 | 29 | /// 30 | /// Required method for Designer support - do not modify 31 | /// the contents of this method with the code editor. 32 | /// 33 | private void InitializeComponent() 34 | { 35 | components = new System.ComponentModel.Container(); 36 | selectedObjectsLabel = new System.Windows.Forms.Label(); 37 | propertyInputBox = new System.Windows.Forms.RoundedTextBox(); 38 | addButton = new System.Windows.Forms.Button(); 39 | ToolTipHint = new System.Windows.Forms.ToolTip(components); 40 | clearButton = new System.Windows.Forms.Button(); 41 | SuspendLayout(); 42 | // 43 | // selectedObjectsLabel 44 | // 45 | selectedObjectsLabel.AutoSize = true; 46 | selectedObjectsLabel.BackColor = Color.Transparent; 47 | selectedObjectsLabel.Font = new Font("Calibri", 9.25F, FontStyle.Bold, GraphicsUnit.Point); 48 | selectedObjectsLabel.ForeColor = Color.FromArgb(255, 231, 143); 49 | selectedObjectsLabel.Location = new Point(12, 8); 50 | selectedObjectsLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); 51 | selectedObjectsLabel.Name = "selectedObjectsLabel"; 52 | selectedObjectsLabel.Size = new Size(54, 15); 53 | selectedObjectsLabel.TabIndex = 4; 54 | selectedObjectsLabel.Text = "Selected"; 55 | ToolTipHint.SetToolTip(selectedObjectsLabel, "SSS"); 56 | // 57 | // propertyInputBox 58 | // 59 | propertyInputBox.BackColor = SystemColors.Info; 60 | propertyInputBox.Font = new Font("Calibri", 8.25F, FontStyle.Regular, GraphicsUnit.Point); 61 | propertyInputBox.Location = new Point(12, 29); 62 | propertyInputBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 63 | propertyInputBox.MaxLength = 64; 64 | propertyInputBox.Name = "propertyInputBox"; 65 | propertyInputBox.Size = new Size(200, 24); 66 | propertyInputBox.TabIndex = 0; 67 | propertyInputBox.KeyDown += PropertyInputBoxKeyDown; 68 | propertyInputBox.MouseEnter += propertyInputBox_MouseEnter; 69 | // 70 | // addButton 71 | // 72 | addButton.AccessibleRole = System.Windows.Forms.AccessibleRole.ButtonDropDown; 73 | addButton.BackColor = Color.Transparent; 74 | addButton.FlatAppearance.BorderSize = 0; 75 | addButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; 76 | addButton.Font = new Font("Microsoft Sans Serif", 7F, FontStyle.Bold, GraphicsUnit.Point); 77 | addButton.ForeColor = Color.Transparent; 78 | addButton.Image = Properties.Resources.add; 79 | addButton.Location = new Point(217, 26); 80 | addButton.Margin = new System.Windows.Forms.Padding(0, 0, 0, 0); 81 | addButton.Name = "addButton"; 82 | addButton.Size = new Size(24, 24); 83 | addButton.TabIndex = 5; 84 | addButton.UseVisualStyleBackColor = true; 85 | addButton.Click += addButton_Click; 86 | addButton.MouseEnter += addButton_MouseEnter; 87 | addButton.MouseLeave += addButton_MouseLeave; 88 | // 89 | // clearButton 90 | // 91 | clearButton.BackColor = Color.Transparent; 92 | clearButton.FlatAppearance.BorderSize = 0; 93 | clearButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; 94 | clearButton.Font = new Font("Microsoft Sans Serif", 7F, FontStyle.Bold, GraphicsUnit.Point); 95 | clearButton.ForeColor = Color.Transparent; 96 | clearButton.Image = Properties.Resources.remove; 97 | clearButton.Location = new Point(241, 26); 98 | clearButton.Margin = new System.Windows.Forms.Padding(0, 0, 0, 0); 99 | clearButton.Name = "clearButton"; 100 | clearButton.Size = new Size(24, 24); 101 | clearButton.TabIndex = 6; 102 | clearButton.UseVisualStyleBackColor = true; 103 | clearButton.Click += clearButton_Click; 104 | clearButton.MouseEnter += clearButton_MouseEnter; 105 | clearButton.MouseLeave += clearButton_MouseLeave; 106 | // 107 | // MainForm 108 | // 109 | AutoScaleDimensions = new SizeF(7F, 15F); 110 | AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 111 | AutoSize = true; 112 | BackColor = Color.FromArgb(46, 46, 46); 113 | ClientSize = new Size(275, 100); 114 | ControlBox = false; 115 | Controls.Add(clearButton); 116 | Controls.Add(addButton); 117 | Controls.Add(propertyInputBox); 118 | Controls.Add(selectedObjectsLabel); 119 | Cursor = System.Windows.Forms.Cursors.Hand; 120 | FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; 121 | Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 122 | MaximizeBox = false; 123 | MinimizeBox = false; 124 | Name = "MainForm"; 125 | ShowIcon = false; 126 | ShowInTaskbar = false; 127 | SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; 128 | Text = "intag"; 129 | TopMost = true; 130 | Deactivate += FormDeactivate; 131 | Load += FormLoad; 132 | MouseDown += FormMouseDown; 133 | ResumeLayout(false); 134 | PerformLayout(); 135 | } 136 | 137 | private System.Windows.Forms.Button clearButton; 138 | 139 | private System.Windows.Forms.ToolTip ToolTipHint; 140 | 141 | private System.Windows.Forms.Button addButton; 142 | 143 | #endregion 144 | 145 | private System.Windows.Forms.Label selectedObjectsLabel; 146 | private System.Windows.Forms.RoundedTextBox propertyInputBox; 147 | } 148 | } 149 | 150 | -------------------------------------------------------------------------------- /intag/intag_form.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Drawing; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Windows.Forms; 8 | 9 | namespace intag 10 | { 11 | internal partial class MainForm : Form 12 | { 13 | private const int WmNclbuttondown = 0xA1; 14 | private const int HtCaption = 0x2; 15 | private Color _accentColor = Color.FromArgb(255, 231, 200); 16 | private Color _disabledColor = Color.FromArgb(120, 100, 120); 17 | 18 | /// 19 | /// Key is object, value is tags assigned to it 20 | /// 21 | private static Dictionary> _selectedTags; 22 | 23 | private static Dictionary> _selectedTagsOnLoad; 24 | private static string[] _objects; 25 | private static SortedSet _tagOptions; 26 | 27 | private enum SelectionState 28 | { 29 | None, 30 | Some, 31 | All 32 | } 33 | 34 | protected override void OnHandleCreated(EventArgs e) 35 | { 36 | WindowUtils.EnableAcrylic(this, Color.Black.WithAlpha(128)); 37 | base.OnHandleCreated(e); 38 | } 39 | 40 | protected override void OnPaintBackground(PaintEventArgs e) 41 | { 42 | e.Graphics.Clear(Color.Transparent); 43 | } 44 | 45 | public MainForm(string[] batch) 46 | { 47 | InitializeComponent(); 48 | if (RegUtils.TryGetSystemColorizationColor(out var accent)) 49 | { 50 | _accentColor = accent; 51 | selectedObjectsLabel.ForeColor = _accentColor; 52 | _disabledColor = accent.WithBrightness(0.5f); 53 | } 54 | _objects = batch.Where(b => !string.IsNullOrWhiteSpace(b)).ToArray(); 55 | 56 | if (_objects.Length == 1) 57 | { 58 | selectedObjectsLabel.Text = _objects[0]; 59 | } 60 | else 61 | { 62 | selectedObjectsLabel.Text = "Multiselect (" + _objects.Length + ")"; 63 | ToolTipHint.SetToolTip(selectedObjectsLabel, string.Join("\n", _objects)); 64 | } 65 | _selectedTags = FileUtils.GetObjectsTags(_objects); 66 | _selectedTagsOnLoad = _selectedTags.ToDictionary(entry => entry.Key, entry => new SortedSet(entry.Value)); 67 | _tagOptions = FileUtils.GetNearbyTags(_objects[0]); 68 | var tagIndex = 0; 69 | foreach (var tagOption in _tagOptions) 70 | { 71 | tagIndex++; 72 | AddDynamicButton(tagIndex, tagOption); 73 | } 74 | ResizeRedraw = true; 75 | AdjustFormHeight(); 76 | } 77 | 78 | private static bool Changed => 79 | !(_selectedTagsOnLoad.All(pair => _selectedTags.ContainsKey(pair.Key) && _selectedTags[pair.Key].SetEquals(pair.Value)) && 80 | _selectedTags.All(pair => _selectedTagsOnLoad.ContainsKey(pair.Key) && _selectedTagsOnLoad[pair.Key].SetEquals(pair.Value))); 81 | 82 | private void AdjustFormHeight() 83 | { 84 | Height = 87 + 25 * (_tagOptions.Count / 2 + 2); 85 | Refresh(); 86 | } 87 | 88 | protected override void OnCreateControl() 89 | { 90 | base.OnCreateControl(); 91 | Region = Region.FromHrgn(CreateRoundRectRgn(2, 3, Width, Height, 3, 3)); 92 | } 93 | 94 | [DllImport("Gdi32.dll", EntryPoint = "CreateRoundRectRgn")] 95 | private static extern IntPtr CreateRoundRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect, int nWidthEllipse, int nHeightEllipse); 96 | 97 | [DllImport("user32.dll")] 98 | private static extern bool ReleaseCapture(); 99 | 100 | [DllImport("user32.dll")] 101 | private static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); 102 | 103 | private static string[] ObjectsWithTag(string tag) 104 | { 105 | return _selectedTags.Where(sel => sel.Value.Contains(tag)).Select(pair => pair.Key).ToArray(); 106 | } 107 | 108 | private static SelectionState TagState(string tag, out string[] withTag) 109 | { 110 | withTag = ObjectsWithTag(tag); 111 | if (withTag.Length == _selectedTags.Count) 112 | { 113 | return SelectionState.All; 114 | } 115 | return withTag.Length > 0 ? SelectionState.Some : SelectionState.None; 116 | 117 | } 118 | 119 | private void UpdateButton(Control button, string tag) 120 | { 121 | var state = TagState(tag, out var withTag); 122 | switch (state) 123 | { 124 | case SelectionState.All: 125 | button.ForeColor = _accentColor; 126 | button.Text = tag; 127 | break; 128 | case SelectionState.Some: 129 | button.ForeColor = _accentColor; 130 | button.Text = $"{tag} (x{withTag.Length})"; 131 | break; 132 | case SelectionState.None: 133 | default: 134 | button.ForeColor = _disabledColor; 135 | button.Text = tag; 136 | break; 137 | } 138 | } 139 | 140 | private void UpdateButtonTooltip(Control button, string tag) 141 | { 142 | var state = TagState(tag, out var withTag); 143 | switch (state) 144 | { 145 | case SelectionState.All: 146 | ToolTipHint.SetToolTip(button, "[All objects]"); 147 | break; 148 | case SelectionState.Some: 149 | ToolTipHint.SetToolTip(button, string.Join("\n", withTag)); 150 | break; 151 | case SelectionState.None: 152 | default: 153 | ToolTipHint.SetToolTip(button, "[None]"); 154 | break; 155 | 156 | } 157 | } 158 | 159 | private void AssignTagToObjects(string tag) 160 | { 161 | Debug.WriteLine($"Assigning tag {tag} to {_objects.Length} objects"); 162 | foreach (var obj in _objects) 163 | { 164 | if (_selectedTags.ContainsKey(obj)) 165 | { 166 | Debug.WriteLine($"Assigning tag {tag} to {obj}"); 167 | _selectedTags[obj].Add(tag); 168 | } 169 | else 170 | { 171 | Debug.WriteLine($"Assigning tag {tag} to {obj} (new set)"); 172 | _selectedTags[obj] = new SortedSet { tag }; 173 | } 174 | } 175 | } 176 | 177 | private void DeleteTagFromObjects(string tag) 178 | { 179 | Debug.WriteLine($"Deleting tag {tag} from {_objects.Length} objects"); 180 | foreach (var obj in _objects) 181 | { 182 | if (_selectedTags.ContainsKey(obj)) 183 | { 184 | Debug.WriteLine($"Deleting tag {tag} from {obj}"); 185 | _selectedTags[obj].Remove(tag); 186 | } 187 | } 188 | } 189 | 190 | private void OnMouseEnter(Button btn, EventArgs e) 191 | { 192 | btn.BackColor = _accentColor; // or Color.Red or whatever you want 193 | btn.ForeColor = Color.White; 194 | } 195 | 196 | 197 | private void OnMouseLeave(Button btn, EventArgs e) 198 | { 199 | btn.BackColor = Color.Transparent; 200 | var state = TagState((string)btn.Tag, out _); 201 | btn.ForeColor = state == SelectionState.None ? _disabledColor : _accentColor; 202 | } 203 | 204 | private void AddDynamicButton(int index, string tag, bool selected = false) 205 | { 206 | var newButton = new Button 207 | { 208 | Text = tag, 209 | Location = new Point(10 + index % 2 * 130, 80 + ((index - 1) / 2 - 1) * 25), 210 | Size = new Size(125, 23), 211 | TabStop = false, 212 | FlatStyle = FlatStyle.Flat, 213 | FlatAppearance = { BorderSize = 0 }, 214 | BackColor = Color.Transparent, 215 | Tag = tag, 216 | Font = new Font("Calibri", 9.25F, FontStyle.Bold, GraphicsUnit.Point, 0), 217 | }; 218 | if (selected) 219 | { 220 | AssignTagToObjects(tag); 221 | } 222 | newButton.MouseEnter += (sender, args) => OnMouseEnter(newButton, args); 223 | newButton.MouseLeave += (sender, args) => OnMouseLeave(newButton, args); 224 | newButton.Click += (sender, args) => 225 | { 226 | Debug.WriteLine($"Clicked on a button: {tag}"); 227 | 228 | //propertyInputBox.Text = value; 229 | var state = TagState(tag, out _); 230 | if (state == SelectionState.None || state == SelectionState.Some) 231 | { 232 | AssignTagToObjects(tag); 233 | } 234 | else 235 | { 236 | DeleteTagFromObjects(tag); 237 | } 238 | UpdateButton(newButton, tag); 239 | UpdateButtonTooltip(newButton, tag); 240 | 241 | //IniUtils.AssignPropertyToFolder(_folder, value, _oldSetOfTagsAsString); 242 | //Environment.Exit(0); 243 | }; 244 | Controls.Add(newButton); 245 | UpdateButton(newButton, tag); 246 | UpdateButtonTooltip(newButton, tag); 247 | } 248 | 249 | private void FormDeactivate(object sender, EventArgs e) 250 | { 251 | if (Changed) 252 | { 253 | FileUtils.AssignTags(_selectedTags); 254 | } 255 | Environment.Exit(1); 256 | } 257 | 258 | private void FormMouseDown(object sender, MouseEventArgs e) 259 | { 260 | if (e.Button != MouseButtons.Left) return; 261 | ReleaseCapture(); 262 | SendMessage(Handle, WmNclbuttondown, HtCaption, 0); 263 | } 264 | 265 | private void FormLoad(object sender, EventArgs e) 266 | { 267 | Left = Cursor.Position.X - Width / 2; 268 | Top = Cursor.Position.Y - Height / 2; 269 | propertyInputBox.Focus(); 270 | propertyInputBox.Select(); 271 | } 272 | 273 | private void PropertyInputBoxKeyDown(object sender, KeyEventArgs e) 274 | { 275 | if (e.KeyCode == Keys.Enter) 276 | { 277 | if (propertyInputBox.Text.Length == 0) FormDeactivate(sender, e); 278 | AddTagOption(); 279 | } 280 | if (e.KeyCode == Keys.Escape) 281 | { 282 | Environment.Exit(0); 283 | } 284 | } 285 | 286 | private void AddTagOption() 287 | { 288 | var tag = propertyInputBox.Text; 289 | if (!_tagOptions.Contains(tag)) 290 | { 291 | Debug.WriteLine($"Adding new tag {tag}"); 292 | _tagOptions.Add(tag); 293 | AddDynamicButton(_tagOptions.Count, tag, true); 294 | } 295 | if (_selectedTags.Any(pair => pair.Value.Contains(tag)) || _tagOptions.Contains(tag)) 296 | { 297 | propertyInputBox.Text = ""; 298 | } 299 | } 300 | 301 | private void addButton_Click(object sender, EventArgs e) 302 | { 303 | AddTagOption(); 304 | } 305 | 306 | private void addButton_MouseEnter(object sender, EventArgs e) 307 | { 308 | addButton.BackColor = _accentColor; 309 | } 310 | 311 | private void addButton_MouseLeave(object sender, EventArgs e) 312 | { 313 | addButton.BackColor = Color.Transparent; 314 | } 315 | 316 | private void clearButton_MouseEnter(object sender, EventArgs e) 317 | { 318 | clearButton.BackColor = _accentColor; 319 | } 320 | 321 | private void clearButton_MouseLeave(object sender, EventArgs e) 322 | { 323 | clearButton.BackColor = Color.Transparent; 324 | } 325 | 326 | private void propertyInputBox_MouseEnter(object sender, EventArgs e) 327 | { 328 | this.ToolTipHint.SetToolTip(this.propertyInputBox, propertyInputBox.Text); 329 | } 330 | 331 | private void clearButton_Click(object sender, EventArgs e) 332 | { 333 | foreach (var key in _selectedTags.Keys.ToArray()) 334 | { 335 | _selectedTags[key] = new SortedSet(); 336 | } 337 | foreach (var control in Controls) 338 | { 339 | if (control is not Button button) continue; 340 | UpdateButton(button, (string)button.Tag); 341 | UpdateButtonTooltip(button, (string)button.Tag); 342 | } 343 | } 344 | } 345 | } -------------------------------------------------------------------------------- /intag/intag_form.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 6, 49 122 | 123 | 124 | 6, 49 125 | 126 | 127 | True 128 | 129 | --------------------------------------------------------------------------------