diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b41d373..9ed6b41 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -67,7 +67,7 @@ Win11Debloat/ │ ├── DefaultSettings.json # Default configuration preset │ ├── Features.json # All features with metadata │ └── LastUsedSettings.json # Last used configuration (generated during use) -├── Regfiles/ # Registry files for each feature +├── Regfiles/ # Registry files for all features └── Schemas/ # XAML Schemas for GUI elements ``` @@ -98,16 +98,16 @@ Avoid these common mistakes when contributing: 1. **Forgetting Get.ps1**: When adding a new command-line parameter, contributors often remember to add it to `Win11Debloat.ps1` but forget to add the same parameter to `Scripts/Get.ps1`. Both files **must** have matching parameters. -2. **Missing Registry Files**: Always create an `Undo` registry file for reversibility, aswell as a `Sysprep` registry file for Sysprep mode. +2. **Missing Registry Files**: Always create an `Undo` registry file for reversibility, aswell as a `Sysprep` registry file for applying changes to other users and Sysprep mode. -3. **Incorrect Registry Hives for Sysprep**: Sysprep registry files apply changes to Windows' default user, registry keys in the `HKEY_CURRENT_USER` hive must use `hkey_users\default` instead. Ensure you update **all** registry keys in the file. +3. **Incorrect Registry Hives for Sysprep**: Sysprep registry files are meant to apply changes to a different user. Registry keys in the `HKEY_CURRENT_USER` hive must use `hkey_users\default` instead. Ensure you update **all** registry keys in the file. 4. **Wrong Registry File Location**: - Main action files go in `Regfiles/` - Undo files go in `Regfiles/Undo/` - Sysprep files go in `Regfiles/Sysprep/` - Placing files in the wrong directory will cause the script to fail when trying to apply or undo changes. + Placing files in the wrong directory may cause the script to fail when trying to apply or undo changes. 6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes. @@ -204,19 +204,20 @@ Add your feature to the `"Features"` array in `Config/Features.json`: ``` **Field Descriptions**: -- `FeatureId`: Unique identifier (must match parameter name in Win11Debloat.ps1 and Get.ps1) -- `Label`: Short description shown in the UI, written in a way to fit with the Action or UndoAction prefixed -- `ToolTip`: Detailed explanation of what the feature does, used for tooltips in the GUI +- `FeatureId`: Unique identifier, this must match parameter name in the Win11Debloat.ps1 and Get.ps1 files. +- `Label`: Short description shown in the UI and wiki documentation. +- `ToolTip`: Detailed explanation of what the feature does, used for tooltips in the GUI. - `Category`: One of the predefined categories (see Categories array in Features.json), features without a category won't be loaded into the GUI. - `Priority`: Optional. The priority value (int) is used to sort features within a category. If this field is omitted the feature will be sorted based on the order in the Features.json file. -- `Action`: Action word for the feature (e.g., "Disable", "Enable", "Hide", "Show") -- `RegistryKey`: Filename of the registry file to apply (in Regfiles/ directory) or null if feature does not require registry changes -- `ApplyText`: Message shown when applying the feature -- `UndoAction`: Action word for reverting (e.g., "Enable", "Show") -- `RegistryUndoKey`: Filename of the registry file to revert changes or null if feature does not require registry changes -- `RequiresReboot`: Optional boolean. Set to `true` if the feature requires a system reboot to take effect -- `MinVersion`: Minimum Windows build version (e.g., "22000") or null -- `MaxVersion`: Maximum Windows version or null +- `RegistryKey`: Filename of the registry file to apply (in Regfiles/ directory) or null if feature does not require registry changes. +- `ApplyText`: Message shown when applying the feature. +- `UndoLabel`: Short description for the undo shown in the UI. +- `ApplyUndoText`: Message shown when undoing the feature. +- `RegistryUndoKey`: Filename of the registry file to revert changes or null if feature does not require registry changes. +- `RequiresReboot`: Optional boolean. Set to `true` if the feature requires a system reboot to take effect. +- `DisableWhenApplied`: Optional boolean. Set to `true` if the feature has no supported undo method. +- `MinVersion`: Minimum Windows build version (e.g., "22000") or null. +- `MaxVersion`: Maximum Windows version or null. #### 3. Add Command-Line Parameter diff --git a/Config/Features.json b/Config/Features.json index d28769c..0cea3c0 100644 --- a/Config/Features.json +++ b/Config/Features.json @@ -1,4 +1,4 @@ -{ +{ "Version": "1.0", "Categories": [ { @@ -280,6 +280,38 @@ ] } ] + }, + { + "GroupId": "StartAllAppsView", + "Label": "Start menu 'All Apps' view", + "ToolTip": "This setting allows you to change the layout of the 'All Apps' section in the start menu, or hide it entirely. Hiding this section may make it harder to find installed apps on your system. This feature uses policies, which will lock down certain settings.", + "Category": "Start Menu & Search", + "Values": [ + { + "Label": "Hide", + "FeatureIds": [ + "DisableStartAllApps" + ] + }, + { + "Label": "Category (Default)", + "FeatureIds": [ + "StartAllAppsCategory" + ] + }, + { + "Label": "Grid", + "FeatureIds": [ + "StartAllAppsGrid" + ] + }, + { + "Label": "List", + "FeatureIds": [ + "StartAllAppsList" + ] + } + ] } ], "Features": [ @@ -289,6 +321,8 @@ "Category": null, "RegistryKey": null, "ApplyText": "Removing selected apps...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": null, "MaxVersion": null @@ -299,6 +333,8 @@ "Category": null, "RegistryKey": null, "ApplyText": "Removing selected apps...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": null, "MaxVersion": null @@ -309,26 +345,8 @@ "Category": null, "RegistryKey": null, "ApplyText": "Removing selected apps...", - "RegistryUndoKey": null, - "MinVersion": null, - "MaxVersion": null - }, - { - "FeatureId": "RemoveCommApps", - "Label": "Remove the Mail, Calendar, and People apps", - "Category": null, - "RegistryKey": null, - "ApplyText": "Removing selected apps...", - "RegistryUndoKey": null, - "MinVersion": null, - "MaxVersion": null - }, - { - "FeatureId": "RemoveW11Outlook", - "Label": "Remove the new Outlook for Windows app", - "Category": null, - "RegistryKey": null, - "ApplyText": "Removing selected apps...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": null, "MaxVersion": null @@ -338,7 +356,9 @@ "Label": "Remove the Xbox App and Xbox Gamebar", "Category": null, "RegistryKey": null, - "ApplyText": "Removing selected apps...", + "ApplyText": "Removing gaming related apps...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": null, "MaxVersion": null @@ -348,7 +368,9 @@ "Label": "Remove HP OEM applications", "Category": null, "RegistryKey": null, - "ApplyText": "Removing selected apps...", + "ApplyText": "Removing HP apps...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": null, "MaxVersion": null @@ -359,6 +381,8 @@ "Category": null, "RegistryKey": null, "ApplyText": null, + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": null, "MaxVersion": null @@ -370,6 +394,8 @@ "Category": "Privacy & Suggested Content", "RegistryKey": "Disable_Telemetry.reg", "ApplyText": "Disabling telemetry, diagnostic data, activity history, app-launch tracking and targeted ads...", + "UndoLabel": "Enable telemetry, tracking & targeted ads", + "ApplyUndoText": "Enabling telemetry, diagnostic data, activity history, app-launch tracking and targeted ads...", "RegistryUndoKey": "Enable_Telemetry.reg", "MinVersion": null, "MaxVersion": null @@ -381,6 +407,8 @@ "Category": "Privacy & Suggested Content", "RegistryKey": "Disable_Windows_Suggestions.reg", "ApplyText": "Disabling tips, tricks, suggestions and ads throughout Windows...", + "UndoLabel": "Enable tips, tricks & suggested content throughout Windows", + "ApplyUndoText": "Enabling tips, tricks, suggestions and ads throughout Windows...", "RegistryUndoKey": "Enable_Windows_Suggestions.reg", "MinVersion": null, "MaxVersion": null @@ -392,6 +420,8 @@ "Category": "Privacy & Suggested Content", "RegistryKey": "Disable_Location_Services.reg", "ApplyText": "Disabling Windows location services and app location access...", + "UndoLabel": "Enable Windows location services & app location access", + "ApplyUndoText": "Enabling Windows location services and app location access...", "RegistryUndoKey": "Enable_Location_Services.reg", "MinVersion": null, "MaxVersion": null @@ -403,6 +433,8 @@ "Category": "Privacy & Suggested Content", "RegistryKey": "Disable_Find_My_Device.reg", "ApplyText": "Disabling Find My Device location tracking...", + "UndoLabel": "Enable Find My Device location tracking", + "ApplyUndoText": "Enabling Find My Device location tracking...", "RegistryUndoKey": "Enable_Find_My_Device.reg", "MinVersion": null, "MaxVersion": null @@ -414,6 +446,8 @@ "Category": "Privacy & Suggested Content", "RegistryKey": "Disable_Lockscreen_Tips.reg", "ApplyText": "Disabling tips & tricks on the lock screen...", + "UndoLabel": "Enable tips & tricks on the lock screen", + "ApplyUndoText": "Enabling tips & tricks on the lock screen...", "RegistryUndoKey": "Enable_Lockscreen_Tips.reg", "MinVersion": null, "MaxVersion": null @@ -425,6 +459,8 @@ "Category": "Privacy & Suggested Content", "RegistryKey": "Disable_Desktop_Spotlight.reg", "ApplyText": "Disabling the 'Windows Spotlight' desktop background option...", + "UndoLabel": "Enable Windows Spotlight for desktop", + "ApplyUndoText": "Enabling the 'Windows Spotlight' desktop background option...", "RegistryUndoKey": "Enable_Desktop_Spotlight.reg", "MinVersion": null, "MaxVersion": null @@ -436,6 +472,8 @@ "Category": "Privacy & Suggested Content", "RegistryKey": "Disable_Edge_Ads_And_Suggestions.reg", "ApplyText": "Disabling ads, suggestions and the MSN news feed in Microsoft Edge...", + "UndoLabel": "Enable ads, suggestions and newsfeed in Edge", + "ApplyUndoText": "Enabling ads, suggestions and the MSN news feed in Microsoft Edge...", "RegistryUndoKey": "Enable_Edge_Ads_And_Suggestions.reg", "MinVersion": null, "MaxVersion": null @@ -447,6 +485,8 @@ "Category": "AI", "RegistryKey": "Disable_Copilot.reg", "ApplyText": "Disabling Microsoft Copilot...", + "UndoLabel": "Enable Microsoft Copilot", + "ApplyUndoText": "Enabling Microsoft Copilot...", "RegistryUndoKey": "Enable_Copilot.reg", "MinVersion": 22621, "MaxVersion": null @@ -458,6 +498,8 @@ "Category": "AI", "RegistryKey": "Disable_AI_Recall.reg", "ApplyText": "Disabling Windows Recall...", + "UndoLabel": "Enable Windows Recall", + "ApplyUndoText": "Enabling Windows Recall...", "RegistryUndoKey": "Enable_AI_Recall.reg", "MinVersion": 22621, "MaxVersion": null @@ -469,6 +511,8 @@ "Category": "AI", "RegistryKey": "Disable_Click_to_Do.reg", "ApplyText": "Disabling Click to Do...", + "UndoLabel": "Enable Click To Do, AI text & image analysis", + "ApplyUndoText": "Enabling Click to Do...", "RegistryUndoKey": "Enable_Click_to_Do.reg", "MinVersion": 22621, "MaxVersion": null @@ -480,6 +524,8 @@ "Category": "AI", "RegistryKey": "Disable_AI_Service_Auto_Start.reg", "ApplyText": "Preventing AI service from starting automatically...", + "UndoLabel": "Allow AI service to start automatically", + "ApplyUndoText": "Allowing AI service to start automatically...", "RegistryUndoKey": "Enable_AI_Service_Auto_Start.reg", "MinVersion": 22621, "MaxVersion": null @@ -491,6 +537,8 @@ "Category": "Gaming", "RegistryKey": "Disable_DVR.reg", "ApplyText": "Disabling Xbox game/screen recording...", + "UndoLabel": "Enable Xbox game/screen recording", + "ApplyUndoText": "Enabling Xbox game/screen recording...", "RegistryUndoKey": "Enable_DVR.reg", "MinVersion": null, "MaxVersion": null @@ -502,6 +550,8 @@ "Category": "Gaming", "RegistryKey": "Disable_Game_Bar_Integration.reg", "ApplyText": "Disabling Game Bar integration...", + "UndoLabel": "Enable Game Bar integration", + "ApplyUndoText": "Enabling Game Bar integration...", "RegistryUndoKey": "Enable_Game_Bar_Integration.reg", "MinVersion": null, "MaxVersion": null @@ -511,7 +561,9 @@ "Label": "Remove all pinned apps from the start menu for this user only", "Category": "Start Menu & Search", "RegistryKey": null, - "ApplyText": null, + "ApplyText": "Removing all pinned apps from the start menu", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22621, "MaxVersion": null @@ -521,7 +573,9 @@ "Label": "Remove all pinned apps from the start menu for all existing and new users", "Category": "Start Menu & Search", "RegistryKey": null, - "ApplyText": null, + "ApplyText": "Removing all pinned apps from the start menu for all users", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22621, "MaxVersion": null @@ -531,7 +585,9 @@ "Label": "Replace the start menu layout for this user only with the provided template file", "Category": null, "RegistryKey": null, - "ApplyText": null, + "ApplyText": "Replacing the start menu", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22621, "MaxVersion": null @@ -541,7 +597,9 @@ "Label": "Replace the start menu layout for all existing and new users with the provided template file", "Category": null, "RegistryKey": null, - "ApplyText": null, + "ApplyText": "Replacing the start menu for all users", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22621, "MaxVersion": null @@ -553,6 +611,8 @@ "Category": "Start Menu & Search", "RegistryKey": "Disable_Start_Recommended.reg", "ApplyText": "Disabling the start menu recommended section...", + "UndoLabel": "Show recommended section in the start menu", + "ApplyUndoText": "Enabling the start menu recommended section...", "RegistryUndoKey": "Enable_Start_Recommended.reg", "MinVersion": 22621, "MaxVersion": null @@ -564,6 +624,8 @@ "Category": "Start Menu & Search", "RegistryKey": "Disable_Start_All_Apps.reg", "ApplyText": "Disabling the 'All Apps' section in the start menu...", + "UndoLabel": "Show 'All Apps' section in the start menu", + "ApplyUndoText": "Enabling the 'All Apps' section in the start menu...", "RegistryUndoKey": "Enable_Start_All_Apps.reg", "MinVersion": 26200, "MaxVersion": null @@ -575,6 +637,8 @@ "Category": "Start Menu & Search", "RegistryKey": "Disable_Phone_Link_In_Start.reg", "ApplyText": "Disabling the Phone Link mobile devices integration in the start menu...", + "UndoLabel": "Enable Phone Link integration in the start menu", + "ApplyUndoText": "Enabling the Phone Link mobile devices integration in the start menu...", "RegistryUndoKey": "Enable_Phone_Link_In_Start.reg", "MinVersion": 22621, "MaxVersion": null @@ -586,6 +650,8 @@ "Category": "Start Menu & Search", "RegistryKey": "Disable_Bing_Cortana_In_Search.reg", "ApplyText": "Disabling Bing web search & Copilot integration in Windows search...", + "UndoLabel": "Enable Bing web search & Copilot integration in search", + "ApplyUndoText": "Enabling Bing web search & Copilot integration in Windows search...", "RegistryUndoKey": "Enable_Bing_Cortana_In_Search.reg", "MinVersion": null, "MaxVersion": null @@ -596,7 +662,9 @@ "ToolTip": "This will disable the Microsoft Store app suggestions in Windows search.", "Category": "Start Menu & Search", "RegistryKey": null, - "ApplyText": null, + "ApplyText": "Disabling Microsoft Store app suggestions in search...", + "UndoLabel": "Enable Microsoft Store app suggestions in search", + "ApplyUndoText": "Enabling Microsoft Store app suggestions in search...", "RegistryUndoKey": null, "MinVersion": 22621, "MaxVersion": null @@ -608,6 +676,8 @@ "Category": "Privacy & Suggested Content", "RegistryKey": "Disable_Settings_365_Ads.reg", "ApplyText": "Disabling Microsoft 365 Copilot ads in Settings Home...", + "UndoLabel": "Show Microsoft 365 Copilot ads in Settings Home", + "ApplyUndoText": "Enabling Microsoft 365 Copilot ads in Settings Home...", "RegistryUndoKey": "Enable_Settings_365_Ads.reg", "MinVersion": null, "MaxVersion": null @@ -619,6 +689,8 @@ "Category": "Other", "RegistryKey": "Disable_Settings_Home.reg", "ApplyText": "Disabling the Settings Home page...", + "UndoLabel": "Show Settings 'Home' page", + "ApplyUndoText": "Enabling the Settings Home page...", "RegistryUndoKey": "Enable_Settings_Home.reg", "MinVersion": null, "MaxVersion": null @@ -630,6 +702,8 @@ "Category": "AI", "RegistryKey": "Disable_Edge_AI_Features.reg", "ApplyText": "Disabling AI features in Microsoft Edge...", + "UndoLabel": "Enable AI features in Microsoft Edge", + "ApplyUndoText": "Enabling AI features in Microsoft Edge...", "RegistryUndoKey": "Enable_Edge_AI_Features.reg", "MinVersion": 22621, "MaxVersion": null @@ -641,6 +715,8 @@ "Category": "AI", "RegistryKey": "Disable_Paint_AI_Features.reg", "ApplyText": "Disabling AI features in Paint...", + "UndoLabel": "Enable AI features in Paint", + "ApplyUndoText": "Enabling AI features in Paint...", "RegistryUndoKey": "Enable_Paint_AI_Features.reg", "MinVersion": 22621, "MaxVersion": null @@ -652,6 +728,8 @@ "Category": "AI", "RegistryKey": "Disable_Notepad_AI_Features.reg", "ApplyText": "Disabling AI features in Notepad...", + "UndoLabel": "Enable AI features in Notepad", + "ApplyUndoText": "Enabling AI features in Notepad...", "RegistryUndoKey": "Enable_Notepad_AI_Features.reg", "MinVersion": 22621, "MaxVersion": null @@ -663,6 +741,8 @@ "Category": "Appearance", "RegistryKey": "Enable_Dark_Mode.reg", "ApplyText": "Enabling dark mode for system and apps...", + "UndoLabel": "Disable dark theme for system and apps", + "ApplyUndoText": "Disabling dark mode for system and apps...", "RegistryUndoKey": "Enable_Light_Mode.reg", "MinVersion": null, "MaxVersion": null @@ -674,6 +754,8 @@ "Category": "System", "RegistryKey": "Disable_Share_Drag_Tray.reg", "ApplyText": "Disabling Drag Tray...", + "UndoLabel": "Enable 'Drag Tray' for sharing & moving files", + "ApplyUndoText": "Enabling Drag Tray...", "RegistryUndoKey": "Enable_Share_Drag_Tray.reg", "MinVersion": 26200, "MaxVersion": null @@ -685,6 +767,8 @@ "Category": "System", "RegistryKey": "Disable_Show_More_Options_Context_Menu.reg", "ApplyText": "Restoring the classic Windows 10 style context menu...", + "UndoLabel": "Use Windows 11 context menu style", + "ApplyUndoText": "Restoring the Windows 11 style context menu...", "RegistryUndoKey": "Enable_W11_Style_Context_Menu.reg", "MinVersion": 22000, "MaxVersion": null @@ -696,6 +780,8 @@ "Category": "System", "RegistryKey": "Disable_Enhance_Pointer_Precision.reg", "ApplyText": "Turning off Enhanced Pointer Precision...", + "UndoLabel": "Enable Enhance Pointer Precision (mouse acceleration)", + "ApplyUndoText": "Turning on Enhanced Pointer Precision...", "RegistryUndoKey": "Enable_Enhance_Pointer_Precision.reg", "RequiresReboot": true, "MinVersion": null, @@ -708,6 +794,8 @@ "Category": "System", "RegistryKey": "Disable_Sticky_Keys_Shortcut.reg", "ApplyText": "Disabling the Sticky Keys keyboard shortcut...", + "UndoLabel": "Enable Sticky Keys keyboard shortcut (5x shift)", + "ApplyUndoText": "Enabling the Sticky Keys keyboard shortcut...", "RegistryUndoKey": "Enable_Sticky_Keys_Shortcut.reg", "RequiresReboot": true, "MinVersion": 26100, @@ -721,6 +809,8 @@ "Priority": 1, "RegistryKey": "Disable_Window_Snapping.reg", "ApplyText": "Disabling window snapping...", + "UndoLabel": "Enable window snapping", + "ApplyUndoText": "Enabling window snapping...", "RegistryUndoKey": "Enable_Window_Snapping.reg", "MinVersion": null, "MaxVersion": null @@ -733,6 +823,8 @@ "Priority": 2, "RegistryKey": "Disable_Snap_Assist.reg", "ApplyText": "Disabling the Snap Assist suggestions...", + "UndoLabel": "Enable showing app suggestions when snapping windows", + "ApplyUndoText": "Enabling the Snap Assist suggestions...", "RegistryUndoKey": "Enable_Snap_Assist.reg", "MinVersion": 22000, "MaxVersion": null @@ -745,6 +837,8 @@ "Priority": 3, "RegistryKey": "Disable_Snap_Layouts.reg", "ApplyText": "Hiding snap layouts when dragging windows to top of the screen and on maximize button...", + "UndoLabel": "Show snap layout flyout at top of screen and on maximize button", + "ApplyUndoText": "Showing snap layouts when dragging windows to top of the screen and on maximize button...", "RegistryUndoKey": "Enable_Snap_Layouts.reg", "MinVersion": 22000, "MaxVersion": null @@ -755,6 +849,8 @@ "Category": "Multi-tasking", "RegistryKey": "Hide_Tabs_In_Alt_Tab.reg", "ApplyText": "Disable showing tabs from apps when snapping or pressing Alt+Tab...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22000, "MaxVersion": null @@ -765,6 +861,8 @@ "Category": "Multi-tasking", "RegistryKey": "Show_3_Tabs_In_Alt_Tab.reg", "ApplyText": "Enable showing 3 tabs from apps when snapping or pressing Alt+Tab...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22000, "MaxVersion": null @@ -775,6 +873,8 @@ "Category": "Multi-tasking", "RegistryKey": "Show_5_Tabs_In_Alt_Tab.reg", "ApplyText": "Enable showing 5 tabs from apps when snapping or pressing Alt+Tab...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22000, "MaxVersion": null @@ -785,6 +885,8 @@ "Category": "Multi-tasking", "RegistryKey": "Show_20_Tabs_In_Alt_Tab.reg", "ApplyText": "Enable showing 20 tabs from apps when snapping or pressing Alt+Tab...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22000, "MaxVersion": null @@ -797,6 +899,8 @@ "Priority": 1, "RegistryKey": "Align_Taskbar_Left.reg", "ApplyText": "Aligning taskbar buttons to the left...", + "UndoLabel": "Align taskbar to the center", + "ApplyUndoText": "Aligning taskbar buttons to the center...", "RegistryUndoKey": "Align_Taskbar_Center.reg", "MinVersion": 22000, "MaxVersion": null @@ -807,6 +911,8 @@ "Category": "Taskbar", "RegistryKey": "Hide_Search_Taskbar.reg", "ApplyText": "Hiding the search icon from the taskbar...", + "UndoLabel": "Show search box on the taskbar", + "ApplyUndoText": "Changing taskbar search to search box...", "RegistryUndoKey": "Show_Search_Box.reg", "MinVersion": 22000, "MaxVersion": null @@ -817,6 +923,8 @@ "Category": "Taskbar", "RegistryKey": "Show_Search_Icon.reg", "ApplyText": "Changing taskbar search to icon only...", + "UndoLabel": "Show search box on the taskbar", + "ApplyUndoText": "Changing taskbar search to search box...", "RegistryUndoKey": "Show_Search_Box.reg", "MinVersion": 22000, "MaxVersion": null @@ -827,6 +935,8 @@ "Category": "Taskbar", "RegistryKey": "Show_Search_Icon_And_Label.reg", "ApplyText": "Changing taskbar search to icon with label...", + "UndoLabel": "Show search box on the taskbar", + "ApplyUndoText": "Changing taskbar search to search box...", "RegistryUndoKey": "Show_Search_Box.reg", "MinVersion": 22000, "MaxVersion": null @@ -837,6 +947,8 @@ "Category": "Taskbar", "RegistryKey": "Show_Search_Box.reg", "ApplyText": "Changing taskbar search to search box...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22000, "MaxVersion": null @@ -849,6 +961,8 @@ "Priority": 3, "RegistryKey": "Hide_Taskview_Taskbar.reg", "ApplyText": "Hiding the taskview button from the taskbar...", + "UndoLabel": "Show 'Task view' button on the taskbar", + "ApplyUndoText": "Showing the taskview button from the taskbar...", "RegistryUndoKey": "Show_Taskview_Taskbar.reg", "MinVersion": 22000, "MaxVersion": null @@ -860,8 +974,11 @@ "Category": "Taskbar", "Priority": 4, "RegistryKey": null, - "ApplyText": null, + "ApplyText": "Disabling widgets on the taskbar & lock screen...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, + "DisableWhenApplied": true, "MinVersion": null, "MaxVersion": null }, @@ -873,6 +990,8 @@ "Priority": 5, "RegistryKey": "Disable_Chat_Taskbar.reg", "ApplyText": "Hiding the chat icon from the taskbar...", + "UndoLabel": "Show Chat (meet now) icon on the taskbar", + "ApplyUndoText": "Showing the chat icon from the taskbar...", "RegistryUndoKey": "Enable_Chat_Taskbar.reg", "MinVersion": null, "MaxVersion": 22621 @@ -884,6 +1003,8 @@ "Category": "System", "RegistryKey": "Disable_Storage_Sense.reg", "ApplyText": "Disabling Storage Sense automatic disk cleanup...", + "UndoLabel": "Enable Storage Sense automatic disk cleanup", + "ApplyUndoText": "Enabling Storage Sense automatic disk cleanup...", "RegistryUndoKey": "Enable_Storage_Sense.reg", "MinVersion": 22000, "MaxVersion": null @@ -895,6 +1016,8 @@ "Category": "System", "RegistryKey": "Disable_Fast_Startup.reg", "ApplyText": "Disabling Fast Start-up...", + "UndoLabel": "Enable fast start-up", + "ApplyUndoText": "Enabling Fast Start-up...", "RegistryUndoKey": "Enable_Fast_Startup.reg", "MinVersion": null, "MaxVersion": null @@ -906,6 +1029,8 @@ "Category": "System", "RegistryKey": "Disable_Bitlocker_Auto_Encryption.reg", "ApplyText": "Disabling BitLocker automatic device encryption...", + "UndoLabel": "Enable BitLocker automatic device encryption", + "ApplyUndoText": "Enabling BitLocker automatic device encryption...", "RegistryUndoKey": "Enable_Bitlocker_Auto_Encryption.reg", "MinVersion": 22000, "MaxVersion": null @@ -917,6 +1042,8 @@ "Category": "System", "RegistryKey": "Disable_Modern_Standby_Networking.reg", "ApplyText": "Disabling network connectivity during Modern Standby...", + "UndoLabel": "Enable Modern Standby network connectivity", + "ApplyUndoText": "Enabling network connectivity during Modern Standby...", "RegistryUndoKey": "Enable_Modern_Standby_Networking.reg", "MinVersion": 22000, "MaxVersion": null @@ -929,6 +1056,8 @@ "Priority": 6, "RegistryKey": "Enable_End_Task.reg", "ApplyText": "Enabling the 'End Task' option in the taskbar right click menu...", + "UndoLabel": "Hide 'End Task' option in taskbar context menu", + "ApplyUndoText": "Disabling the 'End Task' option in the taskbar right click menu...", "RegistryUndoKey": "Disable_End_Task.reg", "MinVersion": 22631, "MaxVersion": null @@ -941,6 +1070,8 @@ "Priority": 7, "RegistryKey": "Enable_Last_Active_Click.reg", "ApplyText": "Enabling the 'Last Active Click' behavior in the taskbar app area...", + "UndoLabel": "Disable 'Last Active Click' behavior for taskbar apps", + "ApplyUndoText": "Disabling the 'Last Active Click' behavior in the taskbar app area...", "RegistryUndoKey": "Disable_Last_Active_Click.reg", "MinVersion": 22000, "MaxVersion": null @@ -951,6 +1082,8 @@ "Category": "Taskbar", "RegistryKey": "Combine_Taskbar_Always.reg", "ApplyText": "Setting the taskbar on the main display to always combine buttons and hide labels...", + "UndoLabel": "Use default taskbar combine behavior", + "ApplyUndoText": "Resetting the taskbar on the main display to always combine buttons and hide labels...", "RegistryUndoKey": null, "MinVersion": 22000, "MaxVersion": null @@ -961,6 +1094,8 @@ "Category": "Taskbar", "RegistryKey": "Combine_MMTaskbar_Always.reg", "ApplyText": "Setting the taskbar on secondary displays to always combine buttons and hide labels...", + "UndoLabel": "Use default taskbar combine behavior", + "ApplyUndoText": "Resetting the taskbar on secondary displays to always combine buttons and hide labels...", "RegistryUndoKey": null, "MinVersion": 22000, "MaxVersion": null @@ -971,6 +1106,8 @@ "Category": "Taskbar", "RegistryKey": "Combine_Taskbar_When_Full.reg", "ApplyText": "Setting the taskbar on the main display to only combine buttons and hide labels when the taskbar is full...", + "UndoLabel": "Always combine taskbar buttons and hide labels for the main display", + "ApplyUndoText": "Setting the taskbar on the main display to always combine buttons and hide labels...", "RegistryUndoKey": "Combine_Taskbar_Always.reg", "MinVersion": 22000, "MaxVersion": null @@ -981,6 +1118,8 @@ "Category": "Taskbar", "RegistryKey": "Combine_MMTaskbar_When_Full.reg", "ApplyText": "Setting the taskbar on secondary displays to only combine buttons and hide labels when the taskbar is full...", + "UndoLabel": "Always combine taskbar buttons and hide labels for secondary displays", + "ApplyUndoText": "Setting the taskbar on secondary displays to always combine buttons and hide labels...", "RegistryUndoKey": "Combine_MMTaskbar_Always.reg", "MinVersion": 22000, "MaxVersion": null @@ -991,6 +1130,8 @@ "Category": "Taskbar", "RegistryKey": "Combine_Taskbar_Never.reg", "ApplyText": "Setting the taskbar on the main display to never combine buttons or hide labels...", + "UndoLabel": "Always combine taskbar buttons and hide labels for the main display", + "ApplyUndoText": "Setting the taskbar on the main display to always combine buttons and hide labels...", "RegistryUndoKey": "Combine_Taskbar_Always.reg", "MinVersion": 22000, "MaxVersion": null @@ -1001,6 +1142,8 @@ "Category": "Taskbar", "RegistryKey": "Combine_MMTaskbar_Never.reg", "ApplyText": "Setting the taskbar on secondary displays to never combine buttons or hide labels...", + "UndoLabel": "Always combine taskbar buttons and hide labels for secondary displays", + "ApplyUndoText": "Setting the taskbar on secondary displays to always combine buttons and hide labels...", "RegistryUndoKey": "Combine_MMTaskbar_Always.reg", "MinVersion": 22000, "MaxVersion": null @@ -1011,6 +1154,8 @@ "Category": "Taskbar", "RegistryKey": "MMTaskbarMode_All.reg", "ApplyText": "Setting the taskbar to show app icons on all taskbars...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": 22000, "MaxVersion": null @@ -1021,6 +1166,8 @@ "Category": "Taskbar", "RegistryKey": "MMTaskbarMode_Main_Active.reg", "ApplyText": "Setting the taskbar to show app icons on main taskbar and on taskbar where the windows is open...", + "UndoLabel": "Show app icons on all taskbars", + "ApplyUndoText": "Setting the taskbar to show app icons on all taskbars...", "RegistryUndoKey": "MMTaskbarMode_All.reg", "MinVersion": 22000, "MaxVersion": null @@ -1031,6 +1178,8 @@ "Category": "Taskbar", "RegistryKey": "MMTaskbarMode_Active.reg", "ApplyText": "Setting the taskbar to only show app icons on the taskbar where the window is open...", + "UndoLabel": "Show app icons on all taskbars", + "ApplyUndoText": "Setting the taskbar to show app icons on all taskbars...", "RegistryUndoKey": "MMTaskbarMode_All.reg", "MinVersion": 22000, "MaxVersion": null @@ -1041,6 +1190,8 @@ "Category": "File Explorer", "RegistryKey": "Launch_File_Explorer_To_Home.reg", "ApplyText": "Changing the default location that File Explorer opens to, to 'Home'...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": null, "MaxVersion": null @@ -1051,6 +1202,8 @@ "Category": "File Explorer", "RegistryKey": "Launch_File_Explorer_To_This_PC.reg", "ApplyText": "Changing the default location that File Explorer opens to, to 'This PC'...", + "UndoLabel": "Change the default location that File Explorer opens to 'Home'", + "ApplyUndoText": "Changing the default location that File Explorer opens to, to 'Home'...", "RegistryUndoKey": "Launch_File_Explorer_To_Home.reg", "MinVersion": null, "MaxVersion": null @@ -1061,6 +1214,8 @@ "Category": "File Explorer", "RegistryKey": "Launch_File_Explorer_To_Downloads.reg", "ApplyText": "Changing the default location that File Explorer opens to, to 'Downloads'...", + "UndoLabel": "Change the default location that File Explorer opens to 'Home'", + "ApplyUndoText": "Changing the default location that File Explorer opens to, to 'Home'...", "RegistryUndoKey": "Launch_File_Explorer_To_Home.reg", "MinVersion": null, "MaxVersion": null @@ -1071,6 +1226,8 @@ "Category": "File Explorer", "RegistryKey": "Launch_File_Explorer_To_OneDrive.reg", "ApplyText": "Changing the default location that File Explorer opens to, to 'OneDrive'...", + "UndoLabel": "Change the default location that File Explorer opens to 'Home'", + "ApplyUndoText": "Changing the default location that File Explorer opens to, to 'Home'...", "RegistryUndoKey": "Launch_File_Explorer_To_Home.reg", "MinVersion": null, "MaxVersion": null @@ -1083,6 +1240,8 @@ "Priority": 2, "RegistryKey": "Show_Extensions_For_Known_File_Types.reg", "ApplyText": "Enabling file extensions for known file types...", + "UndoLabel": "Hide file extensions for known file types", + "ApplyUndoText": "Disabling file extensions for known file types...", "RegistryUndoKey": "Hide_Extensions_For_Known_File_Types.reg", "MinVersion": null, "MaxVersion": null @@ -1095,6 +1254,8 @@ "Priority": 3, "RegistryKey": "Show_Hidden_Folders.reg", "ApplyText": "Unhiding hidden files, folders and drives...", + "UndoLabel": "Hide hidden files, folders and drives", + "ApplyUndoText": "Hiding hidden files, folders and drives...", "RegistryUndoKey": "Hide_Hidden_Folders.reg", "MinVersion": null, "MaxVersion": null @@ -1107,6 +1268,8 @@ "Priority": 4, "RegistryKey": "Hide_duplicate_removable_drives_from_navigation_pane_of_File_Explorer.reg", "ApplyText": "Hiding duplicate removable drive entries from the File Explorer navigation pane...", + "UndoLabel": "Show duplicate removable drive entries", + "ApplyUndoText": "Showing duplicate removable drive entries from the File Explorer navigation pane...", "RegistryUndoKey": "Show_duplicate_removable_drives_in_navigation_pane_of_File_Explorer.reg", "MinVersion": null, "MaxVersion": null @@ -1119,6 +1282,8 @@ "Priority": 5, "RegistryKey": "Hide_Home_from_Explorer.reg", "ApplyText": "Hiding the 'Home' section from the File Explorer navigation pane...", + "UndoLabel": "Show 'Home' from navigation pane", + "ApplyUndoText": "Showing the 'Home' section from the File Explorer navigation pane...", "RegistryUndoKey": "Show_Home_in_Explorer.reg", "MinVersion": 22000, "MaxVersion": null @@ -1131,6 +1296,8 @@ "Priority": 6, "RegistryKey": "Hide_Gallery_from_Explorer.reg", "ApplyText": "Hiding the 'Gallery' section from the File Explorer navigation pane...", + "UndoLabel": "Show 'Gallery' from navigation pane", + "ApplyUndoText": "Showing the 'Gallery' section from the File Explorer navigation pane...", "RegistryUndoKey": "Show_Gallery_in_Explorer.reg", "MinVersion": 22000, "MaxVersion": null @@ -1142,6 +1309,8 @@ "Category": "Appearance", "RegistryKey": "Disable_Transparency.reg", "ApplyText": "Disabling transparency effects...", + "UndoLabel": "Enable transparency effects", + "ApplyUndoText": "Enabling transparency effects...", "RegistryUndoKey": "Enable_Transparency.reg", "MinVersion": null, "MaxVersion": null @@ -1153,6 +1322,8 @@ "Category": "Appearance", "RegistryKey": "Disable_Animations.reg", "ApplyText": "Disabling animations and visual effects...", + "UndoLabel": "Enable animations and visual effects", + "ApplyUndoText": "Enabling animations and visual effects...", "RegistryUndoKey": "Enable_Animations.reg", "RequiresReboot": true, "MinVersion": null, @@ -1165,6 +1336,8 @@ "Category": "Windows Update", "RegistryKey": "Disable_Update_ASAP.reg", "ApplyText": "Preventing Windows from getting updates as soon as they are available...", + "UndoLabel": "Allow getting updates as soon as they're available", + "ApplyUndoText": "Allowing Windows to get updates as soon as they are available...", "RegistryUndoKey": "Enable_Update_ASAP.reg", "MinVersion": null, "MaxVersion": null @@ -1176,6 +1349,8 @@ "Category": "Windows Update", "RegistryKey": "Prevent_Auto_Reboot.reg", "ApplyText": "Preventing automatic restarts after updates while signed in...", + "UndoLabel": "Allow automatic restarts after updates while signed in", + "ApplyUndoText": "Allowing automatic restarts after updates while signed in...", "RegistryUndoKey": "Allow_Auto_Reboot.reg", "MinVersion": null, "MaxVersion": null @@ -1187,6 +1362,8 @@ "Category": "Windows Update", "RegistryKey": "Disable_Delivery_Optimization.reg", "ApplyText": "Disabling sharing of downloaded updates with other PCs...", + "UndoLabel": "Enable sharing downloaded updates with other PCs", + "ApplyUndoText": "Enabling sharing of downloaded updates with other PCs...", "RegistryUndoKey": "Enable_Delivery_Optimization.reg", "MinVersion": null, "MaxVersion": null @@ -1196,7 +1373,9 @@ "Label": "Forcefully uninstall Microsoft Edge. NOT RECOMMENDED!", "Category": null, "RegistryKey": null, - "ApplyText": null, + "ApplyText": "Forcefully uninstalling Microsoft Edge...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": null, "MaxVersion": null @@ -1209,6 +1388,8 @@ "Priority": 7, "RegistryKey": "Hide_Onedrive_Folder.reg", "ApplyText": "Hiding the 'OneDrive' section from the File Explorer navigation pane...", + "UndoLabel": "Show 'OneDrive' from navigation pane", + "ApplyUndoText": "Showing the 'OneDrive' section from the File Explorer navigation pane...", "RegistryUndoKey": "Show_Onedrive_Folder.reg", "MinVersion": null, "MaxVersion": null @@ -1221,6 +1402,8 @@ "Priority": 8, "RegistryKey": "Hide_3D_Objects_Folder.reg", "ApplyText": "Hiding the '3D objects' folder from the File Explorer navigation pane...", + "UndoLabel": "Show '3D objects' folder under 'This PC'", + "ApplyUndoText": "Showing the '3D objects' folder from the File Explorer navigation pane...", "RegistryUndoKey": "Show_3D_Objects_Folder.reg", "MinVersion": null, "MaxVersion": 21999 @@ -1233,6 +1416,8 @@ "Priority": 9, "RegistryKey": "Hide_Music_Folder.reg", "ApplyText": "Hiding the 'Music' folder from the File Explorer navigation pane...", + "UndoLabel": "Show 'Music' folder under 'This PC'", + "ApplyUndoText": "Showing the 'Music' folder from the File Explorer navigation pane...", "RegistryUndoKey": "Show_Music_Folder.reg", "MinVersion": null, "MaxVersion": 21999 @@ -1245,6 +1430,8 @@ "Priority": 10, "RegistryKey": "Add_All_Folders_Under_This_PC.reg", "ApplyText": "Adding all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer...", + "UndoLabel": "Remove common folders back to 'This PC' page", + "ApplyUndoText": "Removing all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer...", "RegistryUndoKey": "Remove_All_Folders_Under_This_PC.reg", "MinVersion": 22000, "MaxVersion": null @@ -1257,6 +1444,8 @@ "Priority": 11, "RegistryKey": "Disable_Include_in_library_from_context_menu.reg", "ApplyText": "Hiding 'Include in library' in the context menu...", + "UndoLabel": "Show 'Include in library' option in the context menu", + "ApplyUndoText": "Showing 'Include in library' in the context menu...", "RegistryUndoKey": "Enable_Include_in_library_in_context_menu.reg", "MinVersion": null, "MaxVersion": 21999 @@ -1269,6 +1458,8 @@ "Priority": 12, "RegistryKey": "Disable_Give_access_to_context_menu.reg", "ApplyText": "Hiding 'Give access to' in the context menu...", + "UndoLabel": "Show 'Give access to' option in the context menu", + "ApplyUndoText": "Showing 'Give access to' in the context menu...", "RegistryUndoKey": "Enable_Give_access_to_context_menu.reg", "MinVersion": null, "MaxVersion": 21999 @@ -1281,6 +1472,8 @@ "Priority": 13, "RegistryKey": "Disable_Share_from_context_menu.reg", "ApplyText": "Hiding 'Share' in the context menu...", + "UndoLabel": "Show 'Share' option in the context menu", + "ApplyUndoText": "Showing 'Share' in the context menu...", "RegistryUndoKey": "Enable_Share_in_context_menu.reg", "MinVersion": null, "MaxVersion": 21999 @@ -1292,6 +1485,8 @@ "Category": "Other", "RegistryKey": "Disable_Brave_Bloat.reg", "ApplyText": "Disabling Brave AI, Crypto, News, Rewards, Talk and VPN in Brave browser...", + "UndoLabel": "Enable Brave browser features (AI, Crypto, etc.)", + "ApplyUndoText": "Enabling Brave AI, Crypto, News, Rewards, Talk and VPN in Brave browser...", "RegistryUndoKey": "Enable_Brave_Bloat.reg", "MinVersion": null, "MaxVersion": null @@ -1303,6 +1498,8 @@ "Category": "Optional Windows Features", "RegistryKey": null, "ApplyText": "Enabling Windows Sandbox...", + "UndoLabel": "Disable Windows Sandbox", + "ApplyUndoText": "Disabling Windows Sandbox...", "RegistryUndoKey": null, "RequiresReboot": true, "MinVersion": 22483, @@ -1315,6 +1512,8 @@ "Category": "Optional Windows Features", "RegistryKey": null, "ApplyText": "Enabling Windows Subsystem for Linux...", + "UndoLabel": "Disable Windows Subsystem for Linux", + "ApplyUndoText": "Disabling Windows Subsystem for Linux...", "RegistryUndoKey": null, "RequiresReboot": true, "MinVersion": 22000, @@ -1327,6 +1526,8 @@ "Category": "File Explorer", "RegistryKey": "Show_Drive_Letters_First.reg", "ApplyText": "Showing drive letters before drive label...", + "UndoLabel": "Show drive letters after drive label", + "ApplyUndoText": "Showing drive letters after drive label...", "RegistryUndoKey": "Show_Drive_Letters_Last.reg", "MinVersion": null, "MaxVersion": null @@ -1338,6 +1539,8 @@ "Category": "File Explorer", "RegistryKey": "Show_Drive_Letters_Last.reg", "ApplyText": "Showing drive letters after drive label...", + "UndoLabel": null, + "ApplyUndoText": null, "RegistryUndoKey": null, "MinVersion": null, "MaxVersion": null @@ -1349,6 +1552,8 @@ "Category": "File Explorer", "RegistryKey": "Show_Network_Drive_Letters_First.reg", "ApplyText": "Showing network drive letters before drive label...", + "UndoLabel": "Show drive letters after drive label", + "ApplyUndoText": "Showing drive letters after drive label...", "RegistryUndoKey": "Show_Drive_Letters_Last.reg", "MinVersion": null, "MaxVersion": null @@ -1360,9 +1565,50 @@ "Category": "File Explorer", "RegistryKey": "Hide_Drive_Letters.reg", "ApplyText": "Hiding all drive letters...", + "UndoLabel": "Show drive letters after drive label", + "ApplyUndoText": "Showing drive letters after drive label...", "RegistryUndoKey": "Show_Drive_Letters_Last.reg", "MinVersion": null, "MaxVersion": null + }, + { + "FeatureId": "StartAllAppsCategory", + "Label": "Show All Apps in Category view (Default)", + "ToolTip": "This will set the All Apps section in the start menu to show apps grouped by category.", + "Category": "Start Menu & Search", + "RegistryKey": "Start_AllApps_Category.reg", + "ApplyText": "Setting All Apps view to Category...", + "UndoLabel": null, + "ApplyUndoText": null, + "RegistryUndoKey": null, + "MinVersion": 26200, + "MaxVersion": null + }, + { + "FeatureId": "StartAllAppsGrid", + "Label": "Show All Apps in Grid view", + "ToolTip": "This will set the All Apps section in the start menu to show apps in an alphabetical grid layout.", + "Category": "Start Menu & Search", + "RegistryKey": "Start_AllApps_Grid.reg", + "ApplyText": "Setting All Apps view to Grid...", + "UndoLabel": "Show All Apps in Category view", + "ApplyUndoText": "Setting All Apps view to Category...", + "RegistryUndoKey": "Start_AllApps_Category.reg", + "MinVersion": 26200, + "MaxVersion": null + }, + { + "FeatureId": "StartAllAppsList", + "Label": "Show All Apps in List view", + "ToolTip": "This will set the All Apps section in the start menu to show apps in an alphabetical list layout.", + "Category": "Start Menu & Search", + "RegistryKey": "Start_AllApps_List.reg", + "ApplyText": "Setting All Apps view to List...", + "UndoLabel": "Show All Apps in Category view", + "ApplyUndoText": "Setting All Apps view to Category...", + "RegistryUndoKey": "Start_AllApps_Category.reg", + "MinVersion": 26200, + "MaxVersion": null } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index b93ac70..c03e322 100755 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ [![Join the Discussion](https://img.shields.io/badge/Join-the%20Discussion-2D9F2D?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Raphire/Win11Debloat/discussions) [![Static Badge](https://img.shields.io/badge/Documentation-_?style=for-the-badge&logo=bookstack&color=grey)](https://github.com/Raphire/Win11Debloat/wiki/) - Win11Debloat is a lightweight, easy to use PowerShell script that allows you to quickly declutter and customize your Windows experience. It can remove pre-installed bloatware apps, disable telemetry, remove intrusive interface elements and much more. No need to painstakingly go through all the settings yourself or remove apps one by one. Win11Debloat makes the process quick and easy! + Win11Debloat is a lightweight, easy to use PowerShell script that allows you to quickly declutter and customize your Windows experience, no installation required! You can use it to remove pre-installed apps, disable telemetry, remove intrusive interface elements and much more. No need to painstakingly go through all the settings yourself or remove apps one by one. Win11Debloat makes the process quick and easy! -The script also includes many features that system administrators and power users will enjoy. Such as a powerful command-line interface, support for Windows Audit mode and the option to make changes to other Windows users. Please refer to our [wiki](https://github.com/Raphire/Win11Debloat/wiki/) for more details. +The script also includes many features that system administrators and power users will enjoy. Such as a powerful command-line interface, support for Windows Audit mode and the ability to make changes to other Windows users. Please refer to our [wiki](https://github.com/Raphire/Win11Debloat/wiki/) for more details. ![Win11Debloat Menu](/Assets/Images/menu.png) diff --git a/Regfiles/Start_AllApps_Category.reg b/Regfiles/Start_AllApps_Category.reg new file mode 100644 index 0000000..9991d5a --- /dev/null +++ b/Regfiles/Start_AllApps_Category.reg @@ -0,0 +1,7 @@ +Windows Registry Editor Version 5.00 + +[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Start] +"AllAppsViewMode"=dword:00000000 + +[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer] +"NoStartMenuMorePrograms"=- diff --git a/Regfiles/Start_AllApps_Grid.reg b/Regfiles/Start_AllApps_Grid.reg new file mode 100644 index 0000000..7816c87 --- /dev/null +++ b/Regfiles/Start_AllApps_Grid.reg @@ -0,0 +1,7 @@ +Windows Registry Editor Version 5.00 + +[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Start] +"AllAppsViewMode"=dword:00000001 + +[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer] +"NoStartMenuMorePrograms"=- diff --git a/Regfiles/Start_AllApps_List.reg b/Regfiles/Start_AllApps_List.reg new file mode 100644 index 0000000..c56a2fb --- /dev/null +++ b/Regfiles/Start_AllApps_List.reg @@ -0,0 +1,7 @@ +Windows Registry Editor Version 5.00 + +[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Start] +"AllAppsViewMode"=dword:00000002 + +[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer] +"NoStartMenuMorePrograms"=- diff --git a/Regfiles/Sysprep/Disable_AI_Recall.reg b/Regfiles/Sysprep/Disable_AI_Recall.reg index e192d1c..15a7ee0 100644 Binary files a/Regfiles/Sysprep/Disable_AI_Recall.reg and b/Regfiles/Sysprep/Disable_AI_Recall.reg differ diff --git a/Regfiles/Sysprep/Disable_Animations.reg b/Regfiles/Sysprep/Disable_Animations.reg index 5025da7..affb4f0 100644 Binary files a/Regfiles/Sysprep/Disable_Animations.reg and b/Regfiles/Sysprep/Disable_Animations.reg differ diff --git a/Regfiles/Sysprep/Disable_Windows_Suggestions.reg b/Regfiles/Sysprep/Disable_Windows_Suggestions.reg index 6bbe821..2aa0e99 100644 Binary files a/Regfiles/Sysprep/Disable_Windows_Suggestions.reg and b/Regfiles/Sysprep/Disable_Windows_Suggestions.reg differ diff --git a/Regfiles/Sysprep/Start_AllApps_Category.reg b/Regfiles/Sysprep/Start_AllApps_Category.reg new file mode 100644 index 0000000..05c03bc --- /dev/null +++ b/Regfiles/Sysprep/Start_AllApps_Category.reg @@ -0,0 +1,7 @@ +Windows Registry Editor Version 5.00 + +[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Start] +"AllAppsViewMode"=dword:00000000 + +[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer] +"NoStartMenuMorePrograms"=- diff --git a/Regfiles/Sysprep/Start_AllApps_Grid.reg b/Regfiles/Sysprep/Start_AllApps_Grid.reg new file mode 100644 index 0000000..cd5c550 --- /dev/null +++ b/Regfiles/Sysprep/Start_AllApps_Grid.reg @@ -0,0 +1,7 @@ +Windows Registry Editor Version 5.00 + +[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Start] +"AllAppsViewMode"=dword:00000001 + +[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer] +"NoStartMenuMorePrograms"=- diff --git a/Regfiles/Sysprep/Start_AllApps_List.reg b/Regfiles/Sysprep/Start_AllApps_List.reg new file mode 100644 index 0000000..5727289 --- /dev/null +++ b/Regfiles/Sysprep/Start_AllApps_List.reg @@ -0,0 +1,7 @@ +Windows Registry Editor Version 5.00 + +[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Start] +"AllAppsViewMode"=dword:00000002 + +[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer] +"NoStartMenuMorePrograms"=- diff --git a/Regfiles/Undo/Enable_AI_Recall.reg b/Regfiles/Undo/Enable_AI_Recall.reg index 907fbf9..beb51f4 100644 Binary files a/Regfiles/Undo/Enable_AI_Recall.reg and b/Regfiles/Undo/Enable_AI_Recall.reg differ diff --git a/Run.bat b/Run.bat index e4debf5..396d4f1 100644 --- a/Run.bat +++ b/Run.bat @@ -19,15 +19,17 @@ if exist "%wtDefaultPath%" ( set "wtPath=" ) +set "SCRIPT_PATH=\"%~dp0Win11Debloat.ps1\"" + :: Launch script if defined wtPath ( call :Log Launching Win11Debloat.ps1 with Windows Terminal... - PowerShell -Command "Start-Process -FilePath '%wtPath%' -ArgumentList 'PowerShell -NoProfile -ExecutionPolicy Bypass -File ""%~dp0Win11Debloat.ps1""' -Verb RunAs" >> "%logFile%" || call :Error "PowerShell command failed" + PowerShell -Command "Start-Process -FilePath '%wtPath%' -ArgumentList 'PowerShell -NoProfile -ExecutionPolicy Bypass -File %SCRIPT_PATH%' -Verb RunAs" >> "%logFile%" || call :Error "PowerShell command failed" call :Log Script execution passed successfully to Win11Debloat.ps1 ) else ( echo Windows Terminal not found. Using default PowerShell instead... call :Log Windows Terminal not found. Using default PowerShell to launch Win11Debloat.ps1... - PowerShell -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File ""%~dp0Win11Debloat.ps1""' -Verb RunAs}" >> "%logFile%" || call :Error "PowerShell command failed" + PowerShell -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File %SCRIPT_PATH%' -Verb RunAs}" >> "%logFile%" || call :Error "PowerShell command failed" call :Log Script execution passed successfully to Win11Debloat.ps1 ) diff --git a/Schemas/MainWindow.xaml b/Schemas/MainWindow.xaml index ee1f1c0..6d2d86c 100644 --- a/Schemas/MainWindow.xaml +++ b/Schemas/MainWindow.xaml @@ -58,7 +58,7 @@ @@ -485,48 +485,81 @@ - + - - + + - + + Fill="{DynamicResource AccentColor}" Width="40" Height="40" Stretch="Uniform"/> + Fill="{DynamicResource AccentColor}" Width="22" Height="22" Stretch="Uniform"/> + Fill="{DynamicResource AccentColor}" Width="17" Height="17" Stretch="Uniform"/> - - - - + + - - - - - + - - + + @@ -732,6 +765,8 @@ + + @@ -798,7 +833,9 @@ - + + + - - - - - - - - - - - - - + + - + diff --git a/Schemas/RestoreBackupWindow.xaml b/Schemas/RestoreBackupWindow.xaml index 1807953..4373d0f 100644 --- a/Schemas/RestoreBackupWindow.xaml +++ b/Schemas/RestoreBackupWindow.xaml @@ -222,6 +222,8 @@ + + @@ -282,9 +284,38 @@ Visibility="Collapsed" Text="This will restore the Start Menu pinned apps layout for the current user."/> - + - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -343,7 +374,7 @@ LineHeight="20" Margin="0,0,0,12" Foreground="{DynamicResource FgColor}" - Text="This will restore the Start Menu pinned apps layout for the selected user(s) using a backup. Win11Debloat can automatically find the backup created by the script."/> + Text="This will restore the Start Menu pinned apps layout for the selected user(s) using a backup that is automatically created by Win11Debloat. Manually created backups can also be used."/> Removing selected apps for $(GetFriendlyTargetUserName)..." + Write-Host "> $($feature.ApplyText) for $(GetFriendlyTargetUserName)..." $appsList = GenerateAppsList if ($appsList.Count -eq 0) { @@ -46,7 +46,7 @@ function ExecuteParameter { RemoveApps $appsList } 'RemoveAppsCustom' { - Write-Host "> Removing selected apps..." + Write-Host "> $($feature.ApplyText)..." $appsList = LoadAppsFromFile $script:CustomAppsListFilePath if ($appsList.Count -eq 0) { @@ -58,58 +58,46 @@ function ExecuteParameter { Write-Host "$($appsList.Count) apps selected for removal" RemoveApps $appsList } - 'RemoveCommApps' { - $appsList = 'Microsoft.windowscommunicationsapps', 'Microsoft.People' - Write-Host "> Removing Mail, Calendar and People apps..." - RemoveApps $appsList - return - } - 'RemoveW11Outlook' { - $appsList = 'Microsoft.OutlookForWindows' - Write-Host "> Removing new Outlook for Windows app..." - RemoveApps $appsList - return - } 'RemoveGamingApps' { - $appsList = 'Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay' - Write-Host "> Removing gaming related apps..." + $appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay') + Write-Host "> $($feature.ApplyText)..." RemoveApps $appsList return } 'RemoveHPApps' { - $appsList = 'AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl' - Write-Host "> Removing HP apps..." + $appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl') + Write-Host "> $($feature.ApplyText)..." RemoveApps $appsList return } 'DisableWidgets' { - Write-Host "> Disabling widgets on the taskbar & lock screen..." + Write-Host "> $($feature.ApplyText)..." # Stop widgets related processes before removing the app packages to prevent potential issues - Get-Process *Widget* | Stop-Process - - RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime' + Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process + + RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime') } - "EnableWindowsSandbox" { - Write-Host "> Enabling Windows Sandbox..." + 'EnableWindowsSandbox' { + Write-Host "> $($feature.ApplyText)..." EnableWindowsFeature "Containers-DisposableClientVM" Write-Host "" return } - "EnableWindowsSubsystemForLinux" { - Write-Host "> Enabling Windows Subsystem for Linux..." + 'EnableWindowsSubsystemForLinux' { + Write-Host "> $($feature.ApplyText)..." EnableWindowsFeature "VirtualMachinePlatform" EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux" Write-Host "" return } 'ClearStart' { - Write-Host "> Removing all pinned apps from the start menu for user $(GetUserName)..." + Write-Host "> $($feature.ApplyText) for user $(GetUserName)..." ReplaceStartMenu Write-Host "" return } 'ReplaceStart' { - Write-Host "> Replacing the start menu for user $(GetUserName)..." + Write-Host "> $($feature.ApplyText) for user $(GetUserName)..." ReplaceStartMenu $script:Params.Item("ReplaceStart") Write-Host "" return @@ -169,8 +157,15 @@ function ExecuteAllChanges { break } } + # Undo operations that write registry values also require a backup + if (-not $hasRegistryBackedFeature) { + foreach ($featureId in $script:UndoParams.Keys) { + $f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null } + if ($f -and $f.RegistryUndoKey) { $hasRegistryBackedFeature = $true; break } + } + } - $totalSteps = $actionableKeys.Count + $totalSteps = $actionableKeys.Count + $script:UndoParams.Count if ($hasRegistryBackedFeature) { $totalSteps++ } if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ } $currentStep = 0 @@ -183,7 +178,13 @@ function ExecuteAllChanges { Write-Host "> Creating registry backup..." try { - New-RegistrySettingsBackup -ActionableKeys $actionableKeys | Out-Null + $undoSyntheticFeatures = @($script:UndoParams.Keys | ForEach-Object { + $f = if ($script:Features.ContainsKey($_)) { $script:Features[$_] } else { $null } + if ($f -and $f.RegistryUndoKey) { + [PSCustomObject]@{ FeatureId = $_; RegistryKey = (Resolve-UndoRegFilePath $f.RegistryUndoKey) } + } + } | Where-Object { $_ }) + New-RegistrySettingsBackup -ActionableKeys $actionableKeys -ExtraFeatures $undoSyntheticFeatures | Out-Null } catch { throw "Registry backup failed before applying changes. $($_.Exception.Message)" @@ -203,9 +204,7 @@ function ExecuteAllChanges { # Execute all parameters foreach ($paramKey in $actionableKeys) { - if ($script:CancelRequested) { - return - } + if ($script:CancelRequested) { return } $currentStep++ @@ -229,8 +228,82 @@ function ExecuteAllChanges { ExecuteParameter -paramKey $paramKey } + # Execute all undo operations + foreach ($featureId in $script:UndoParams.Keys) { + if ($script:CancelRequested) { return } + + $f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null } + $undoLabel = if ($f -and $f.UndoLabel) { $f.UndoLabel } else { $featureId } + $applyUndoText = if ($f -and $f.ApplyUndoText) { $f.ApplyUndoText } else { $undoLabel } + + $currentStep++ + if ($script:ApplyProgressCallback) { + & $script:ApplyProgressCallback $currentStep $totalSteps $applyUndoText + } + + if ($f -and $f.RegistryUndoKey) { + ImportRegistryFile "> $applyUndoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey) + } else { + Invoke-UndoFeatureAction -FeatureId $featureId + } + } + if ($script:RegistryImportFailures -gt 0) { Write-Host "" Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow } -} \ No newline at end of file +} + +# Resolves the path of an undo reg file relative to $script:RegfilesPath. +# Checks the Undo/ subfolder first, then falls back to the root Regfiles/ folder. +function Resolve-UndoRegFilePath { + param ([string]$FileName) + $undoSubPath = Join-Path 'Undo' $FileName + if (Test-Path (Join-Path $script:RegfilesPath $undoSubPath)) { + return $undoSubPath + } + return $FileName +} + +function Invoke-UndoFeatureAction { + param( + [Parameter(Mandatory)] + [string]$FeatureId + ) + + $feature = if ($script:Features.ContainsKey($FeatureId)) { $script:Features[$FeatureId] } else { $null } + + switch ($FeatureId) { + 'DisableStoreSearchSuggestions' { + if ($script:Params.ContainsKey('Sysprep')) { + Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..." + EnableStoreSearchSuggestionsForAllUsers + Write-Host "" + return + } + + Write-Host "> Re-enabling Microsoft Store search suggestions for user $(GetUserName)..." + EnableStoreSearchSuggestions + Write-Host "" + return + } + 'EnableWindowsSandbox' { + Write-Host "> $($feature.ApplyUndoText)..." + DisableWindowsFeature 'Containers-DisposableClientVM' + Write-Host "" + return + } + 'EnableWindowsSubsystemForLinux' { + Write-Host "> $($feature.ApplyUndoText)..." + DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux' + DisableWindowsFeature 'VirtualMachinePlatform' + Write-Host "" + return + } + default { + Write-Host "> No undo action defined for $FeatureId, skipping..." -ForegroundColor Yellow + Write-Host "" + return + } + } +} diff --git a/Scripts/Features/GetCurrentTweakState.ps1 b/Scripts/Features/GetCurrentTweakState.ps1 new file mode 100644 index 0000000..498ffb8 --- /dev/null +++ b/Scripts/Features/GetCurrentTweakState.ps1 @@ -0,0 +1,175 @@ +# Tests whether the registry operations in a feature's .reg file currently match the live registry. +# Returns $true if ALL operations in the apply reg file match current system state. +# Returns $false if the feature has no RegistryKey, the file is missing, or any operation mismatches. +function Get-ExpectedRegistryValueKind { + param( + [Parameter(Mandatory)] + $Operation + ) + + switch ([string]$Operation.ValueType) { + 'DWord' { return [Microsoft.Win32.RegistryValueKind]::DWord } + 'QWord' { return [Microsoft.Win32.RegistryValueKind]::QWord } + 'String' { return [Microsoft.Win32.RegistryValueKind]::String } + 'Binary' { return [Microsoft.Win32.RegistryValueKind]::Binary } + 'Hex2' { return [Microsoft.Win32.RegistryValueKind]::ExpandString } + 'Hex7' { return [Microsoft.Win32.RegistryValueKind]::MultiString } + default { return $null } + } +} + +function Test-FeatureApplied { + param ( + [Parameter(Mandatory)] + [string]$FeatureId + ) + + if (-not $script:Features.ContainsKey($FeatureId)) { return $false } + $feature = $script:Features[$FeatureId] + + switch ($FeatureId) { + 'DisableWidgets' { + # Widgets packages cannot be reinstalled automatically, so we treat their + # absence as the applied state (checked) and presence as not-yet-applied. + $widgetAppIds = @( + 'Microsoft.StartExperiencesApp', + 'MicrosoftWindows.Client.WebExperience', + 'Microsoft.WidgetsPlatformRuntime' + ) + + foreach ($appId in $widgetAppIds) { + if (Get-AppxPackage -Name $appId -AllUsers -ErrorAction SilentlyContinue) { + return $false + } + } + + return $true + } + 'DisableStoreSearchSuggestions' { + if ($script:Params.ContainsKey('Sysprep')) { + return (Test-StoreSearchSuggestionsDisabledForAllUsers) + } + + $storeDbPath = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" + if ($script:Params.ContainsKey('User')) { + $storeDbPath = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false + } + + return (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDbPath) + } + 'EnableWindowsSandbox' { + return (Test-WindowsOptionalFeatureEnabled -FeatureName 'Containers-DisposableClientVM') + } + 'EnableWindowsSubsystemForLinux' { + $wslEnabled = Test-WindowsOptionalFeatureEnabled -FeatureName 'Microsoft-Windows-Subsystem-Linux' + $vmpEnabled = Test-WindowsOptionalFeatureEnabled -FeatureName 'VirtualMachinePlatform' + return ($wslEnabled -and $vmpEnabled) + } + } + + if (-not $feature.RegistryKey) { return $false } + + $regFilePath = Join-Path $script:RegfilesPath $feature.RegistryKey + if (-not (Test-Path $regFilePath)) { return $false } + + try { + $operations = @(Get-RegFileOperations -regFilePath $regFilePath) + } + catch { return $false } + + if ($operations.Count -eq 0) { return $false } + + foreach ($op in $operations) { + $parts = Split-RegistryPath -path $op.KeyPath + if (-not $parts) { return $false } + + $rootKey = Get-RegistryRootKey -hiveName $parts.Hive + if (-not $rootKey) { return $false } + + $key = $null + try { + $key = $rootKey.OpenSubKey($parts.SubKey, $false) + + switch ($op.OperationType) { + 'DeleteKey' { + if ($null -ne $key) { return $false } + } + 'DeleteValue' { + if ($null -ne $key) { + $names = @($key.GetValueNames()) + if ($names -icontains $op.ValueName) { return $false } + } + # key missing = value also gone = operation matches + } + 'SetValue' { + if ($null -eq $key) { return $false } + $names = @($key.GetValueNames()) + if (-not ($names -icontains $op.ValueName)) { return $false } + + $actualKind = $key.GetValueKind($op.ValueName) + $expectedKind = Get-ExpectedRegistryValueKind -Operation $op + if ($null -eq $expectedKind -or $actualKind -ne $expectedKind) { return $false } + $actualRaw = $key.GetValue($op.ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) + + $actual = switch ($actualKind) { + ([Microsoft.Win32.RegistryValueKind]::DWord) { + [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$actualRaw), 0) + } + ([Microsoft.Win32.RegistryValueKind]::QWord) { + [BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$actualRaw), 0) + } + ([Microsoft.Win32.RegistryValueKind]::Binary) { + @($actualRaw | ForEach-Object { [int]$_ }) + } + ([Microsoft.Win32.RegistryValueKind]::MultiString) { + @($actualRaw) + } + default { + if ($null -ne $actualRaw) { [string]$actualRaw } else { $null } + } + } + + $expected = $op.ValueData + + $match = if (($actual -is [array]) -and ($expected -is [array])) { + (Compare-Object $actual $expected).Count -eq 0 + } else { + $actual -eq $expected + } + + if (-not $match) { return $false } + } + } + } + catch { return $false } + finally { + if ($null -ne $key) { $key.Close() } + } + } + + return $true +} + +# Returns the 1-based index of the UiGroup option whose features all match current system state, +# or 0 if no option fully matches (meaning the current state is unknown / "No Change"). +function Get-CurrentGroupActiveIndex { + param ( + [Parameter(Mandatory)] + [object]$Group + ) + + $i = 1 + foreach ($val in $Group.Values) { + $allApplied = $true + foreach ($fid in $val.FeatureIds) { + if (-not (Test-FeatureApplied -FeatureId $fid)) { + $allApplied = $false + break + } + } + if ($allApplied) { return $i } + $i++ + } + + return 0 +} diff --git a/Scripts/Features/ImportRegistryFile.ps1 b/Scripts/Features/ImportRegistryFile.ps1 index 0e1e9a7..1bfb73c 100644 --- a/Scripts/Features/ImportRegistryFile.ps1 +++ b/Scripts/Features/ImportRegistryFile.ps1 @@ -8,13 +8,7 @@ function ImportRegistryFile { Write-Host $message $usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User") - $regFileDirectory = if ($usesOfflineHive) { - Join-Path $script:RegfilesPath "Sysprep" - } - else { - $script:RegfilesPath - } - $regFilePath = Join-Path $regFileDirectory $path + $regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path if (-not (Test-Path $regFilePath)) { $errorMessage = "Unable to find registry file: $path ($regFilePath)" diff --git a/Scripts/Features/RegistryBackupValidation.ps1 b/Scripts/Features/RegistryBackupValidation.ps1 index b3c8624..d74feeb 100644 --- a/Scripts/Features/RegistryBackupValidation.ps1 +++ b/Scripts/Features/RegistryBackupValidation.ps1 @@ -33,12 +33,49 @@ function Get-NormalizedSelectedFeatureIdsFromBackup { $errors.Add('SelectedFeatures must contain non-empty string feature IDs.') } - if ($selectedFeatures.Count -eq 0) { - $errors.Add('SelectedFeatures must contain at least one feature ID.') + return [PSCustomObject]@{ + SelectedFeatures = $selectedFeatures.ToArray() + Errors = $errors.ToArray() + } +} + +function Get-NormalizedSelectedUndoFeatureIdsFromBackup { + param( + [Parameter(Mandatory)] + $Backup + ) + + $selectedUndoFeatures = New-Object System.Collections.Generic.List[string] + $selectedUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) + $errors = New-Object System.Collections.Generic.List[string] + + # SelectedUndoFeatures is optional - only process if present + if (-not $Backup.PSObject.Properties['SelectedUndoFeatures']) { + return [PSCustomObject]@{ + SelectedUndoFeatures = $selectedUndoFeatures.ToArray() + Errors = $errors.ToArray() + } + } + + $hasInvalidSelectedUndoFeatureId = $false + foreach ($featureId in @($Backup.SelectedUndoFeatures)) { + if ($featureId -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$featureId)) { + $hasInvalidSelectedUndoFeatureId = $true + continue + } + + $normalizedFeatureId = [string]$featureId + if ($selectedUndoFeatureIds.Add($normalizedFeatureId)) { + $selectedUndoFeatures.Add($normalizedFeatureId) + } + } + + if ($hasInvalidSelectedUndoFeatureId) { + $errors.Add('SelectedUndoFeatures must contain non-empty string feature IDs.') } return [PSCustomObject]@{ - SelectedFeatures = $selectedFeatures.ToArray() + SelectedUndoFeatures = $selectedUndoFeatures.ToArray() Errors = $errors.ToArray() } } @@ -96,6 +133,9 @@ function Test-RegistryBackupMatchesSelectedFeatures { [AllowEmptyCollection()] [string[]]$SelectedFeatureIds, [Parameter(Mandatory)] + [AllowEmptyCollection()] + [string[]]$SelectedUndoFeatureIds, + [Parameter(Mandatory)] [string]$Target, [Parameter(Mandatory)] [AllowEmptyCollection()] @@ -109,18 +149,19 @@ function Test-RegistryBackupMatchesSelectedFeatures { return $errors.ToArray() } - $selectedRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedFeatureIds) -Errors $errors) + $selectedRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedFeatureIds) -IsUndoFeature:$false -Errors $errors) + $undoRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedUndoFeatureIds) -IsUndoFeature:$true -Errors $errors) $useSysprepRegFiles = ($Target -eq 'DefaultUserProfile') -or ($Target -like 'User:*') $capturePlans = @() - if ($errors.Count -eq 0 -and $selectedRegistryFeatures.Count -gt 0) { - $capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles) + if ($errors.Count -eq 0 -and ($selectedRegistryFeatures.Count -gt 0 -or $undoRegistryFeatures.Count -gt 0)) { + $capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UndoRegistryFeatures @($undoRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles) } $planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans) if ($planMap.Count -eq 0 -and @($RegistryKeys).Count -gt 0) { - $errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from SelectedFeatures.') + $errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from the selected features.') } foreach ($rootSnapshot in @($RegistryKeys)) { @@ -136,6 +177,8 @@ function Get-SelectedRegistryFeaturesForBackupValidation { [AllowEmptyCollection()] [string[]]$SelectedFeatureIds, [Parameter(Mandatory)] + [bool]$IsUndoFeature, + [Parameter(Mandatory)] [AllowEmptyCollection()] $Errors ) @@ -152,7 +195,26 @@ function Get-SelectedRegistryFeaturesForBackupValidation { } $feature = $script:Features[$featureId] - if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) { + if (-not $feature) { + continue + } + + # For undo features, check RegistryUndoKey if present (real features) + # Otherwise check RegistryKey (for synthetic features from backup capture) + $registryKeyToUse = if ($IsUndoFeature) { + $key = [string]$feature.RegistryUndoKey + if (-not [string]::IsNullOrWhiteSpace($key)) { + $key + } + else { + [string]$feature.RegistryKey + } + } + else { + [string]$feature.RegistryKey + } + + if (-not [string]::IsNullOrWhiteSpace($registryKeyToUse)) { $selectedRegistryFeatures.Add($feature) } } diff --git a/Scripts/Features/ReplaceStartMenu.ps1 b/Scripts/Features/ReplaceStartMenu.ps1 index 712b1ef..c476a92 100644 --- a/Scripts/Features/ReplaceStartMenu.ps1 +++ b/Scripts/Features/ReplaceStartMenu.ps1 @@ -39,7 +39,7 @@ function ReplaceStartMenuForAllUsers { } -# Replace the startmenu for all users, when using the default startmenuTemplate this clears all pinned apps +# Replace the startmenu at the specified location, when using the default startmenuTemplate this clears all pinned apps # Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/ function ReplaceStartMenu { param ( diff --git a/Scripts/Features/RestoreRegistryBackup.ps1 b/Scripts/Features/RestoreRegistryBackup.ps1 index 3ae84af..002fbb0 100644 --- a/Scripts/Features/RestoreRegistryBackup.ps1 +++ b/Scripts/Features/RestoreRegistryBackup.ps1 @@ -87,7 +87,17 @@ function Normalize-RegistryBackup { $errors.Add([string]$selectedFeatureParseError) } - $allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys)) + $selectedUndoFeatureParseResult = Get-NormalizedSelectedUndoFeatureIdsFromBackup -Backup $Backup + $selectedUndoFeatures = @($selectedUndoFeatureParseResult.SelectedUndoFeatures) + foreach ($selectedUndoFeatureParseError in @($selectedUndoFeatureParseResult.Errors)) { + $errors.Add([string]$selectedUndoFeatureParseError) + } + + $allSelectedFeatures = @($selectedFeatures) + @($selectedUndoFeatures) + if ($allSelectedFeatures.Count -eq 0) { + $errors.Add('Backup must contain at least one feature ID in SelectedFeatures or SelectedUndoFeatures.') + } + $allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -SelectedUndoFeatureIds @($selectedUndoFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys)) foreach ($allowListValidationError in $allowListValidationErrors) { $errors.Add([string]$allowListValidationError) } @@ -110,6 +120,7 @@ function Normalize-RegistryBackup { ComputerName = [string]$Backup.ComputerName Target = $normalizedTarget SelectedFeatures = @($selectedFeatures) + SelectedUndoFeatures = @($selectedUndoFeatures) RegistryKeys = @($normalizedKeys) } } diff --git a/Scripts/Features/WindowsOptionalFeatures.ps1 b/Scripts/Features/WindowsOptionalFeatures.ps1 new file mode 100644 index 0000000..f1361c3 --- /dev/null +++ b/Scripts/Features/WindowsOptionalFeatures.ps1 @@ -0,0 +1,49 @@ +# Enables a Windows optional feature and pipes its output to the console +function EnableWindowsFeature { + param ( + [string]$FeatureName + ) + + $result = Invoke-NonBlocking -ScriptBlock { + param($name) + Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart + } -ArgumentList $FeatureName + + $dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] } + if ($dismResult) { + Write-Host ($dismResult | Out-String).Trim() + } +} + +# Disables a Windows optional feature and pipes its output to the console +function DisableWindowsFeature { + param ( + [string]$FeatureName + ) + + $result = Invoke-NonBlocking -ScriptBlock { + param($name) + Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart + } -ArgumentList $FeatureName + + $dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] } + if ($dismResult) { + Write-Host ($dismResult | Out-String).Trim() + } +} + +function Test-WindowsOptionalFeatureEnabled { + param ( + [Parameter(Mandatory)] + [string]$FeatureName + ) + + try { + $feature = Get-WindowsOptionalFeature -Online -FeatureName $FeatureName -ErrorAction Stop + } + catch { + return $false + } + + return ($feature.State -eq 'Enabled') +} \ No newline at end of file diff --git a/Scripts/GUI/MainWindow-AppSelection.ps1 b/Scripts/GUI/MainWindow-AppSelection.ps1 new file mode 100644 index 0000000..8293ffe --- /dev/null +++ b/Scripts/GUI/MainWindow-AppSelection.ps1 @@ -0,0 +1,540 @@ +# MainWindow-AppSelection.ps1 +# App-selection panel functions: tri-state helpers, sorting, search/highlight, app loading, preset management, and removal scope. + +function Add-TriStateClickBehavior { + param([System.Windows.Controls.CheckBox]$CheckBox) + + if (-not $CheckBox -or -not $CheckBox.IsThreeState) { return } + + if (-not $CheckBox.PSObject.Properties['WasIndeterminateBeforeClick']) { + Add-Member -InputObject $CheckBox -MemberType NoteProperty -Name 'WasIndeterminateBeforeClick' -Value $false + } + + $CheckBox.Add_PreviewMouseLeftButtonDown({ + $this.WasIndeterminateBeforeClick = ($this.IsChecked -eq [System.Nullable[bool]]$null) + }) +} + +function ConvertTo-NormalizedCheckboxState { + param([System.Windows.Controls.CheckBox]$CheckBox) + + if ($CheckBox.PSObject.Properties['WasIndeterminateBeforeClick'] -and $CheckBox.WasIndeterminateBeforeClick) { + # WPF toggles null -> false before Click handlers fire; restore desired mixed -> checked behavior. + $CheckBox.WasIndeterminateBeforeClick = $false + $CheckBox.IsChecked = $true + return $true + } + + return ($CheckBox.IsChecked -eq $true) +} + +function Set-TriStatePresetCheckBoxState { + param( + [System.Windows.Controls.CheckBox]$CheckBox, + [int]$Total, + [int]$Selected + ) + + if (-not $CheckBox) { return } + + if ($Total -eq 0) { + $CheckBox.IsEnabled = $false + $CheckBox.IsChecked = $false + return + } + + $CheckBox.IsEnabled = $true + if ($Selected -eq 0) { + $CheckBox.IsChecked = $false + } + elseif ($Selected -eq $Total) { + $CheckBox.IsChecked = $true + } + else { + $CheckBox.IsChecked = [System.Nullable[bool]]$null + } +} + +function Update-SortArrows { + param( + [System.Windows.Controls.TextBlock]$SortArrowName, + [System.Windows.Controls.TextBlock]$SortArrowDescription, + [System.Windows.Controls.TextBlock]$SortArrowAppId + ) + + $ease = New-Object System.Windows.Media.Animation.CubicEase + $ease.EasingMode = 'EaseOut' + $arrows = @{ + 'Name' = $SortArrowName + 'Description' = $SortArrowDescription + 'AppId' = $SortArrowAppId + } + foreach ($col in $arrows.Keys) { + $tb = $arrows[$col] + # Active column: full opacity, rotate to indicate direction (0 = up/asc, 180 = down/desc) + # Inactive columns: dim, reset to 0 + if ($col -eq $script:SortColumn) { + $targetAngle = if ($script:SortAscending) { 0 } else { 180 } + $tb.Opacity = 1.0 + } + else { + $targetAngle = 0 + $tb.Opacity = 0.3 + } + $anim = New-Object System.Windows.Media.Animation.DoubleAnimation + $anim.To = $targetAngle + $anim.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200)) + $anim.EasingFunction = $ease + $tb.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $anim) + } +} + +function Update-AppsPanelRebuildSearchIndex { + param( + [System.Windows.Controls.Panel]$AppsPanel, + $ActiveMatch = $null + ) + + $newMatches = @() + $newActiveIndex = -1 + $i = 0 + foreach ($child in $AppsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox] -and $child.Background -ne [System.Windows.Media.Brushes]::Transparent) { + $newMatches += $child + if ($null -ne $ActiveMatch -and [System.Object]::ReferenceEquals($child, $ActiveMatch)) { + $newActiveIndex = $i + } + $i++ + } + } + $script:AppSearchMatches = $newMatches + $script:AppSearchMatchIndex = if ($newActiveIndex -ge 0) { $newActiveIndex } elseif ($newMatches.Count -gt 0) { 0 } else { -1 } +} + +function Update-AppsPanelSort { + param( + [System.Windows.Controls.Panel]$AppsPanel, + [System.Windows.Controls.TextBlock]$SortArrowName, + [System.Windows.Controls.TextBlock]$SortArrowDescription, + [System.Windows.Controls.TextBlock]$SortArrowAppId + ) + + $children = @($AppsPanel.Children) + $key = switch ($script:SortColumn) { + 'Name' { { $_.AppName } } + 'Description' { { $_.AppDescription } } + 'AppId' { { $_.AppIdDisplay } } + } + $sorted = $children | Sort-Object $key -Descending:(-not $script:SortAscending) + $AppsPanel.Children.Clear() + foreach ($checkbox in $sorted) { + $AppsPanel.Children.Add($checkbox) | Out-Null + } + Update-SortArrows -SortArrowName $SortArrowName -SortArrowDescription $SortArrowDescription -SortArrowAppId $SortArrowAppId + + # Rebuild search match list in new sorted order so keyboard navigation stays correct + if ($script:AppSearchMatches.Count -gt 0) { + $activeMatch = if ($script:AppSearchMatchIndex -ge 0 -and $script:AppSearchMatchIndex -lt $script:AppSearchMatches.Count) { + $script:AppSearchMatches[$script:AppSearchMatchIndex] + } + else { $null } + Update-AppsPanelRebuildSearchIndex -AppsPanel $AppsPanel -ActiveMatch $activeMatch + } +} + +function Update-AppSelectionStatus { + param( + [System.Windows.Controls.Panel]$AppsPanel, + [System.Windows.Controls.TextBlock]$AppSelectionStatus, + [System.Windows.Controls.ComboBox]$AppRemovalScopeCombo, + [System.Windows.Controls.Border]$AppRemovalScopeSection, + [System.Windows.Controls.TextBlock]$AppRemovalScopeDescription, + [System.Windows.Controls.ComboBox]$UserSelectionCombo + ) + + $selectedCount = 0 + foreach ($child in $AppsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { + $selectedCount++ + } + } + $AppSelectionStatus.Text = "$selectedCount app(s) selected for removal" + + if ($AppRemovalScopeCombo -and $AppRemovalScopeSection -and $AppRemovalScopeDescription) { + if ($selectedCount -gt 0) { + $AppRemovalScopeSection.Visibility = 'Visible' + if ($UserSelectionCombo.SelectedIndex -ne 2) { + $AppRemovalScopeCombo.IsEnabled = $true + } + Update-AppRemovalScopeDescription -AppRemovalScopeCombo $AppRemovalScopeCombo -AppRemovalScopeDescription $AppRemovalScopeDescription + } + else { + $AppRemovalScopeSection.Visibility = 'Collapsed' + } + } +} + +function Update-AppRemovalScopeDescription { + param( + [System.Windows.Controls.ComboBox]$AppRemovalScopeCombo, + [System.Windows.Controls.TextBlock]$AppRemovalScopeDescription + ) + + $selectedItem = $AppRemovalScopeCombo.SelectedItem + if ($selectedItem) { + switch ($selectedItem.Content) { + "All users" { + $AppRemovalScopeDescription.Text = "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users." + } + "Current user only" { + $AppRemovalScopeDescription.Text = "Apps will only be removed for the current user. Existing and new users will not be affected." + } + "Target user only" { + $AppRemovalScopeDescription.Text = "Apps will only be removed for the specified target user. Existing and new users will not be affected." + } + } + } +} + +function Invoke-AppPreset { + param( + [System.Windows.Controls.Panel]$AppsPanel, + [scriptblock]$MatchFilter, + [bool]$Check, + [switch]$Exclusive + ) + + foreach ($child in $AppsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox]) { + if ($Exclusive) { + $child.IsChecked = (& $MatchFilter $child) + } + elseif (& $MatchFilter $child) { + $child.IsChecked = $Check + } + } + } + Update-AppPresetStates -AppsPanel $AppsPanel +} + +function Update-AppPresetStates { + param([System.Windows.Controls.Panel]$AppsPanel) + + $script:UpdatingPresets = $true + try { + # Helper: count matching and checked apps, set checkbox state + function SetPresetState($CheckBox, [scriptblock]$MatchFilter) { + $total = 0; $checked = 0 + foreach ($child in $AppsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox]) { + if (& $MatchFilter $child) { + $total++ + if ($child.IsChecked) { $checked++ } + } + } + } + Set-TriStatePresetCheckBoxState -CheckBox $CheckBox -Total $total -Selected $checked + } + + # Find preset checkboxes via window + $window = $script:MainWindow + $presetDefaultApps = $window.FindName('PresetDefaultApps') + $presetLastUsed = $window.FindName('PresetLastUsed') + + SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true } + foreach ($jsonCb in $script:JsonPresetCheckboxes) { + $localIds = $jsonCb.PresetAppIds + SetPresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure() + } + + # Last used preset: only update if it's visible (has saved apps) + if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) { + SetPresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } + } + } + finally { + $script:UpdatingPresets = $false + } +} + +function Scroll-ToItemIfNotVisible { + param ( + [System.Windows.Controls.ScrollViewer]$ScrollViewer, + [System.Windows.UIElement]$Item, + [System.Windows.UIElement]$Container + ) + + if (-not $ScrollViewer -or -not $Item -or -not $Container) { return } + + try { + $itemPosition = $Item.TransformToAncestor($Container).Transform([System.Windows.Point]::new(0, 0)).Y + $viewportHeight = $ScrollViewer.ViewportHeight + $itemHeight = $Item.ActualHeight + $currentOffset = $ScrollViewer.VerticalOffset + + # Check if the item is currently visible in the viewport + $itemTop = $itemPosition - $currentOffset + $itemBottom = $itemTop + $itemHeight + + $isVisible = ($itemTop -ge 0) -and ($itemBottom -le $viewportHeight) + + # Only scroll if the item is not visible + if (-not $isVisible) { + # Center the item in the viewport + $targetOffset = $itemPosition - ($viewportHeight / 2) + ($itemHeight / 2) + $ScrollViewer.ScrollToVerticalOffset([Math]::Max(0, $targetOffset)) + } + } + catch { + # Fallback to simple bring into view + $Item.BringIntoView() + } +} + +function Find-ParentScrollViewer { + param ([System.Windows.UIElement]$Element) + + $parent = [System.Windows.Media.VisualTreeHelper]::GetParent($Element) + while ($null -ne $parent) { + if ($parent -is [System.Windows.Controls.ScrollViewer]) { + return $parent + } + $parent = [System.Windows.Media.VisualTreeHelper]::GetParent($parent) + } + return $null +} + +function Load-AppsWithList { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.Panel]$AppsPanel, + [System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox, + [System.Windows.Controls.Border]$LoadingAppsIndicator, + [System.Windows.Controls.MenuItem]$ImportConfigBtn, + [string]$ListOfApps + ) + + $script:MainWindowLastSelectedCheckbox = $null + + $loaderScriptPath = $script:LoadAppsDetailsScriptPath + $appsFilePath = $script:AppsListFilePath + $onlyInstalled = [bool]$OnlyInstalledAppsBox.IsChecked + + # Use preloaded data if available; otherwise load in background job + if (-not $onlyInstalled -and $script:PreloadedAppData) { + $rawAppData = $script:PreloadedAppData + $script:PreloadedAppData = $null + } + else { + # Load apps details in a background job to keep the UI responsive + $rawAppData = Invoke-NonBlocking -ScriptBlock { + param($loaderScript, $appsListFilePath, $installedList, $onlyInstalled) + $script:AppsListFilePath = $appsListFilePath + . $loaderScript + LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false + } -ArgumentList $loaderScriptPath, $appsFilePath, $ListOfApps, $onlyInstalled + } + + $appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName) + + $LoadingAppsIndicator.Visibility = 'Collapsed' + + if ($appsToAdd.Count -eq 0) { + $OnlyInstalledAppsBox.IsHitTestVisible = $true + $Window.FindName('DeploymentApplyBtn').IsEnabled = $true + if ($ImportConfigBtn) { + $ImportConfigBtn.IsEnabled = $true + } + return + } + + $brushSafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#4CAF50') + $brushUnsafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#F44336') + $brushDefault = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#FFC107') + $brushSafe.Freeze(); $brushUnsafe.Freeze(); $brushDefault.Freeze() + + # Create WPF controls; pump the Dispatcher every batch so the spinner keeps animating. + $batchSize = 20 + for ($i = 0; $i -lt $appsToAdd.Count; $i++) { + $app = $appsToAdd[$i] + + $checkbox = New-Object System.Windows.Controls.CheckBox + $automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppIdDisplay) { $app.AppIdDisplay } else { $null } + if ($automationName) { $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $automationName) } + $checkbox.Tag = $app.AppIdDisplay + $checkbox.IsChecked = $app.IsChecked + $checkbox.Style = $Window.Resources['AppsPanelCheckBoxStyle'] + + # Build table row: Recommendation dot | Name | Description | App ID + $row = New-Object System.Windows.Controls.Grid + $row.Style = $Window.Resources['AppTableRowStyle'] + $c0 = New-Object System.Windows.Controls.ColumnDefinition; $c0.Width = $Window.Resources['AppTableDotColWidth'] + $c1 = New-Object System.Windows.Controls.ColumnDefinition; $c1.Width = $Window.Resources['AppTableNameColWidth'] + $c2 = New-Object System.Windows.Controls.ColumnDefinition; $c2.Width = $Window.Resources['AppTableDescColWidth'] + $c3 = New-Object System.Windows.Controls.ColumnDefinition; $c3.Width = $Window.Resources['AppTableIdColWidth'] + $row.ColumnDefinitions.Add($c0); $row.ColumnDefinitions.Add($c1) + $row.ColumnDefinitions.Add($c2); $row.ColumnDefinitions.Add($c3) + + $dot = New-Object System.Windows.Shapes.Ellipse + $dot.Style = $Window.Resources['AppRecommendationDotStyle'] + $dot.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } } + $dot.ToolTip = switch ($app.Recommendation) { + 'safe' { '[Recommended] Safe to remove for most users' } + 'unsafe' { '[Not Recommended] Only remove if you know what you are doing' } + default { "[Optional] Remove if you don't need this app" } + } + [System.Windows.Controls.Grid]::SetColumn($dot, 0) + + $tbName = New-Object System.Windows.Controls.TextBlock + $tbName.Text = $app.FriendlyName + $tbName.Style = $Window.Resources['AppNameTextStyle'] + [System.Windows.Controls.Grid]::SetColumn($tbName, 1) + + $tbDesc = New-Object System.Windows.Controls.TextBlock + $tbDesc.Text = $app.Description + $tbDesc.Style = $Window.Resources['AppDescTextStyle'] + $tbDesc.ToolTip = $app.Description + [System.Windows.Controls.Grid]::SetColumn($tbDesc, 2) + + $tbId = New-Object System.Windows.Controls.TextBlock + $tbId.Text = $app.AppIdDisplay + $tbId.Style = $Window.Resources["AppIdTextStyle"] + $tbId.ToolTip = $app.AppIdDisplay + [System.Windows.Controls.Grid]::SetColumn($tbId, 3) + + $row.Children.Add($dot) | Out-Null + $row.Children.Add($tbName) | Out-Null + $row.Children.Add($tbDesc) | Out-Null + $row.Children.Add($tbId) | Out-Null + $checkbox.Content = $row + + Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppName' -Value $app.FriendlyName + Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppDescription' -Value $app.Description + Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'SelectedByDefault' -Value $app.SelectedByDefault + Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($app.AppId) + Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIdDisplay' -Value $app.AppIdDisplay + + $checkbox.Add_Checked({ + $w = $script:MainWindow + Update-AppSelectionStatus -AppsPanel $w.FindName('AppSelectionPanel') ` + -AppSelectionStatus $w.FindName('AppSelectionStatus') ` + -AppRemovalScopeCombo $w.FindName('AppRemovalScopeCombo') ` + -AppRemovalScopeSection $w.FindName('AppRemovalScopeSection') ` + -AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') ` + -UserSelectionCombo $w.FindName('UserSelectionCombo') + }) + $checkbox.Add_Unchecked({ + $w = $script:MainWindow + Update-AppSelectionStatus -AppsPanel $w.FindName('AppSelectionPanel') ` + -AppSelectionStatus $w.FindName('AppSelectionStatus') ` + -AppRemovalScopeCombo $w.FindName('AppRemovalScopeCombo') ` + -AppRemovalScopeSection $w.FindName('AppRemovalScopeSection') ` + -AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') ` + -UserSelectionCombo $w.FindName('UserSelectionCombo') + }) + AttachShiftClickBehavior -checkbox $checkbox -appsPanel $AppsPanel ` + -lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) ` + -updateStatusCallback { + $w = $script:MainWindow + Update-AppSelectionStatus -AppsPanel $w.FindName('AppSelectionPanel') ` + -AppSelectionStatus $w.FindName('AppSelectionStatus') ` + -AppRemovalScopeCombo $w.FindName('AppRemovalScopeCombo') ` + -AppRemovalScopeSection $w.FindName('AppRemovalScopeSection') ` + -AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') ` + -UserSelectionCombo $w.FindName('UserSelectionCombo') + } + + $AppsPanel.Children.Add($checkbox) | Out-Null + + if (($i + 1) % $batchSize -eq 0) { DoEvents } + } + + $sortArrowName = $Window.FindName('SortArrowName') + $sortArrowDescription = $Window.FindName('SortArrowDescription') + $sortArrowAppId = $Window.FindName('SortArrowAppId') + Update-AppsPanelSort -AppsPanel $AppsPanel -SortArrowName $sortArrowName -SortArrowDescription $sortArrowDescription -SortArrowAppId $sortArrowAppId + + # If Default Mode was clicked while apps were still loading, apply defaults now + if ($script:PendingDefaultMode) { + $script:PendingDefaultMode = $false + Invoke-AppPreset -AppsPanel $AppsPanel -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive + } + + $appSelectionStatusText = $Window.FindName('AppSelectionStatus') + $appRemovalScopeCombo = $Window.FindName('AppRemovalScopeCombo') + $appRemovalScopeSection = $Window.FindName('AppRemovalScopeSection') + $appRemovalScopeDescription = $Window.FindName('AppRemovalScopeDescription') + $userSelectionCombo = $Window.FindName('UserSelectionCombo') + Update-AppSelectionStatus -AppsPanel $AppsPanel -AppSelectionStatus $appSelectionStatusText ` + -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection ` + -AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo + + # Re-enable controls now that the full, correctly-checked app list is ready + $OnlyInstalledAppsBox.IsHitTestVisible = $true + $Window.FindName('DeploymentApplyBtn').IsEnabled = $true + if ($ImportConfigBtn) { + $ImportConfigBtn.IsEnabled = $true + } +} + +function Load-AppsIntoMainUI { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.Panel]$AppsPanel, + [System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox, + [System.Windows.Controls.Border]$LoadingAppsIndicator, + [System.Windows.Controls.MenuItem]$ImportConfigBtn + ) + + # Prevent concurrent loads + if ($script:IsLoadingApps) { return } + $script:IsLoadingApps = $true + + if ($ImportConfigBtn) { + $ImportConfigBtn.IsEnabled = $false + } + + # Show loading indicator and clear existing apps + $LoadingAppsIndicator.Visibility = 'Visible' + $AppsPanel.Children.Clear() + + # Disable controls while apps are loading so they can't be interacted with mid-load + $Window.FindName('DeploymentApplyBtn').IsEnabled = $false + $OnlyInstalledAppsBox.IsHitTestVisible = $false + + # Update navigation buttons to disable Next/Previous + Update-NavigationButtons -Window $Window -TabControl $Window.FindName('MainTabControl') + + # Force a render so the loading indicator is visible, then schedule the + # actual loading at Background priority so this call returns immediately. + # This is critical when called from Add_Loaded: the window must finish + # its initialization before we start a nested message pump via DoEvents. + $Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action] {}) + $Window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action] { + try { + $listOfApps = "" + + if ($OnlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) { + $listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking + + if ($null -eq $listOfApps) { + Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null + $OnlyInstalledAppsBox.IsChecked = $false + } + } + + Load-AppsWithList -Window $Window -AppsPanel $AppsPanel -OnlyInstalledAppsBox $OnlyInstalledAppsBox ` + -LoadingAppsIndicator $LoadingAppsIndicator -ImportConfigBtn $ImportConfigBtn -ListOfApps $listOfApps + } + catch { + Write-Warning "Failed to load apps list: $($_.Exception.Message)" + $LoadingAppsIndicator.Visibility = 'Collapsed' + $OnlyInstalledAppsBox.IsHitTestVisible = $true + $Window.FindName('DeploymentApplyBtn').IsEnabled = $true + if ($ImportConfigBtn) { $ImportConfigBtn.IsEnabled = $true } + } + finally { + $script:IsLoadingApps = $false + } + }) | Out-Null +} diff --git a/Scripts/GUI/MainWindow-Deployment.ps1 b/Scripts/GUI/MainWindow-Deployment.ps1 new file mode 100644 index 0000000..3c26c29 --- /dev/null +++ b/Scripts/GUI/MainWindow-Deployment.ps1 @@ -0,0 +1,487 @@ +# MainWindow-Deployment.ps1 +# Overview generation, pending tweak actions, feature labels, tweak preset maps, apply logic, user mode state, user selection, and validation. + +function Get-FeatureLabel { + param( + [string]$FeatureId, + $FallbackLabel = $null + ) + + $label = $script:FeatureLabelLookup[$FeatureId] + if (-not [string]::IsNullOrWhiteSpace([string]$label)) { + return [string]$label + } + + if (-not [string]::IsNullOrWhiteSpace([string]$FallbackLabel)) { + return [string]$FallbackLabel + } + + return [string]$FeatureId +} + +function Get-UndoFeatureLabel { + param( + [string]$FeatureId, + $FallbackLabel = $null + ) + + $undoLabel = $script:UndoFeatureLabelLookup[$FeatureId] + if (-not [string]::IsNullOrWhiteSpace([string]$undoLabel)) { + return [string]$undoLabel + } + + # Fall back to the regular label (prefixed for undo context) + $label = Get-FeatureLabel -FeatureId $FeatureId -FallbackLabel $FallbackLabel + return [string]$label +} + +function Get-PendingTweakActions { + param( + [System.Windows.Window]$Window, + [bool]$ShowAppliedTweaksMode + ) + + $actions = New-Object System.Collections.Generic.List[object] + + if (-not $script:UiControlMappings) { + return @($actions.ToArray()) + } + + foreach ($mappingKey in $script:UiControlMappings.Keys) { + $control = $Window.FindName($mappingKey) + if (-not $control) { continue } + $mapping = $script:UiControlMappings[$mappingKey] + + if ($control -is [System.Windows.Controls.CheckBox] -and $mapping.Type -eq 'feature') { + $wasApplied = $false + if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemState']) { + $wasApplied = [bool]$control.SystemState + } + elseif ($null -ne $control.PSObject.Properties['InitialState']) { + $wasApplied = [bool]$control.InitialState + } + elseif ($null -ne $control.PSObject.Properties['SystemState']) { + $wasApplied = [bool]$control.SystemState + } + $isNowChecked = $control.IsChecked -eq $true + + if (-not $wasApplied -and $isNowChecked) { + $actions.Add([PSCustomObject]@{ + Action = 'Apply' + FeatureId = [string]$mapping.FeatureId + Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label) + }) + } + elseif ($wasApplied -and -not $isNowChecked) { + $actions.Add([PSCustomObject]@{ + Action = 'Undo' + FeatureId = [string]$mapping.FeatureId + Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label) + }) + } + } + elseif ($control -is [System.Windows.Controls.ComboBox] -and $mapping.Type -eq 'group') { + $wasIndex = 0 + if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemIndex']) { + $wasIndex = [int]$control.SystemIndex + } + elseif ($null -ne $control.PSObject.Properties['InitialIndex']) { + $wasIndex = [int]$control.InitialIndex + } + elseif ($null -ne $control.PSObject.Properties['SystemIndex']) { + $wasIndex = [int]$control.SystemIndex + } + $isNowIndex = $control.SelectedIndex + + if ($wasIndex -eq $isNowIndex) { continue } + + if ($isNowIndex -gt 0 -and $isNowIndex -le $mapping.Values.Count) { + $selectedValue = $mapping.Values[$isNowIndex - 1] + foreach ($fid in $selectedValue.FeatureIds) { + $actions.Add([PSCustomObject]@{ + Action = 'Apply' + FeatureId = [string]$fid + Label = (Get-FeatureLabel -FeatureId $fid) + }) + } + } + } + } + + return @($actions.ToArray()) +} + +function New-Overview { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.StackPanel]$AppsPanel, + $ShowCurrentlyAppliedTweaksCheckBox + ) + + $changesList = @() + $showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) + + # Collect selected apps + $selectedAppsCount = 0 + foreach ($child in $AppsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { + $selectedAppsCount++ + } + } + if ($selectedAppsCount -gt 0) { + $changesList += "Remove $selectedAppsCount application(s)" + } + + foreach ($tweakAction in @(Get-PendingTweakActions -Window $Window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) { + if ($tweakAction.Action -eq 'Undo') { + $changesList += "Undo: $($tweakAction.Label)" + } + else { + $changesList += $tweakAction.Label + } + } + + return $changesList +} + +function Invoke-ShowChangesOverview { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.StackPanel]$AppsPanel, + $ShowCurrentlyAppliedTweaksCheckBox + ) + + $changesList = New-Overview -Window $Window -AppsPanel $AppsPanel -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox + + if ($changesList.Count -eq 0) { + Show-MessageBox -Message 'No changes have been selected.' -Title 'Selected Changes' -Button 'OK' -Icon 'Information' + return + } + + $message = ($changesList | ForEach-Object { "$([char]0x2022) $_" }) -join "`n" + Show-MessageBox -Message $message -Title 'Selected Changes' -Button 'OK' -Icon 'None' -Width 600 +} + +function Build-TweakPresetControlMap { + param( + [System.Windows.Window]$Window, + $SettingsJson + ) + + $presetMap = @{} + if (-not $SettingsJson -or -not $SettingsJson.Settings -or -not $script:UiControlMappings) { + return $presetMap + } + + # FeatureId -> control metadata, similar to ApplySettingsToUiControls lookup. + $featureIdIndex = @{} + foreach ($controlName in $script:UiControlMappings.Keys) { + $control = $Window.FindName($controlName) + if (-not $control -or $control.Visibility -ne 'Visible') { continue } + + $mapping = $script:UiControlMappings[$controlName] + if ($mapping.Type -eq 'group') { + $i = 1 + foreach ($val in $mapping.Values) { + foreach ($fid in $val.FeatureIds) { + $featureIdIndex[$fid] = @{ ControlName = $controlName; Control = $control; MappingType = 'group'; Index = $i } + } + $i++ + } + } + elseif ($mapping.Type -eq 'feature') { + $featureIdIndex[$mapping.FeatureId] = @{ ControlName = $controlName; Control = $control; MappingType = 'feature' } + } + } + + foreach ($setting in $SettingsJson.Settings) { + if ($setting.Value -ne $true) { continue } + if ($setting.Name -eq 'CreateRestorePoint') { continue } + + $entry = $featureIdIndex[$setting.Name] + if (-not $entry) { continue } + if ($presetMap.ContainsKey($entry.ControlName)) { continue } + + $controlType = if ($entry.Control -is [System.Windows.Controls.CheckBox]) { 'CheckBox' } else { 'ComboBox' } + $desiredValue = switch ($entry.MappingType) { + 'group' { $entry.Index } + default { if ($controlType -eq 'CheckBox') { $true } else { 1 } } + } + + $presetMap[$entry.ControlName] = @{ Control = $entry.Control; ControlType = $controlType; DesiredValue = $desiredValue } + } + + return $presetMap +} + +function Build-CategoryTweakPresetMap { + param( + [System.Windows.Window]$Window, + [string]$Category + ) + + $presetMap = @{} + if (-not $script:UiControlMappings) { return $presetMap } + + foreach ($controlName in $script:UiControlMappings.Keys) { + $mapping = $script:UiControlMappings[$controlName] + if ($mapping.Category -ne $Category) { continue } + + $control = $Window.FindName($controlName) + if (-not $control -or $control.Visibility -ne 'Visible') { continue } + + $controlType = if ($control -is [System.Windows.Controls.CheckBox]) { 'CheckBox' } else { 'ComboBox' } + $desiredValue = if ($controlType -eq 'CheckBox') { $true } else { 1 } + $presetMap[$controlName] = @{ Control = $control; ControlType = $controlType; DesiredValue = $desiredValue } + } + + return $presetMap +} + +function Get-SavedAppIdsFromSettingsJson { + param($SettingsJson) + + if (-not $SettingsJson -or -not $SettingsJson.Settings) { + return $null + } + + $appsValue = $null + foreach ($setting in $SettingsJson.Settings) { + if ($setting.Name -eq 'Apps' -and $setting.Value) { + $appsValue = $setting.Value + break + } + } + + if (-not $appsValue) { + return $null + } + + $savedAppIds = @() + if ($appsValue -is [string]) { + $savedAppIds = $appsValue.Split(',') + } + elseif ($appsValue -is [array]) { + $savedAppIds = $appsValue + } + + $savedAppIds = $savedAppIds | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + if ($savedAppIds.Count -eq 0) { + return $null + } + + return $savedAppIds +} + +function Invoke-ApplyTweakPresetMap { + param( + [hashtable]$PresetMap, + [bool]$Check + ) + + if (-not $PresetMap) { + $PresetMap = @{} + } + + $wasUpdatingTweakPresets = [bool]$script:UpdatingTweakPresets + $script:UpdatingTweakPresets = $true + try { + foreach ($target in $PresetMap.Values) { + $control = $target.Control + if (-not $control) { continue } + + if ($target.ControlType -eq 'CheckBox') { + $control.IsChecked = $Check + } + elseif ($target.ControlType -eq 'ComboBox') { + $desiredIndex = [int]$target.DesiredValue + if ($Check) { + $control.SelectedIndex = $desiredIndex + } + elseif ($control.SelectedIndex -eq $desiredIndex) { + $control.SelectedIndex = 0 + } + } + } + } + finally { + $script:UpdatingTweakPresets = $wasUpdatingTweakPresets + } + + if (-not $wasUpdatingTweakPresets) { + Update-TweakPresetStates -Window $script:MainWindow + } +} + +function Set-TweakPresetCheckBoxState { + param( + [System.Windows.Controls.CheckBox]$PresetCheckBox, + [hashtable]$PresetMap + ) + + if (-not $PresetCheckBox) { return } + if (-not $PresetMap) { + $PresetMap = @{} + } + + $total = $PresetMap.Count + $selected = 0 + + foreach ($target in $PresetMap.Values) { + $control = $target.Control + if (-not $control) { continue } + + if ($target.ControlType -eq 'CheckBox' -and $control.IsChecked -eq $true) { + $selected++ + } + elseif ($target.ControlType -eq 'ComboBox' -and $control.SelectedIndex -eq [int]$target.DesiredValue) { + $selected++ + } + } + + Set-TriStatePresetCheckBoxState -CheckBox $PresetCheckBox -Total $total -Selected $selected +} + +function Update-TweakPresetStates { + param([System.Windows.Window]$Window) + + $script:UpdatingTweakPresets = $true + try { + $presetDefaultTweaksBtn = $Window.FindName('PresetDefaultTweaksBtn') + $presetLastUsedTweaksBtn = $Window.FindName('PresetLastUsedTweaksBtn') + $presetPrivacyTweaksBtn = $Window.FindName('PresetPrivacyTweaksBtn') + $presetAITweaksBtn = $Window.FindName('PresetAITweaksBtn') + + Set-TweakPresetCheckBoxState -PresetCheckBox $presetDefaultTweaksBtn -PresetMap $script:DefaultTweakPresetMap + if ($presetLastUsedTweaksBtn -and $presetLastUsedTweaksBtn.Visibility -ne 'Collapsed') { + Set-TweakPresetCheckBoxState -PresetCheckBox $presetLastUsedTweaksBtn -PresetMap $script:LastUsedTweakPresetMap + } + Set-TweakPresetCheckBoxState -PresetCheckBox $presetPrivacyTweaksBtn -PresetMap $script:PrivacyTweakPresetMap + Set-TweakPresetCheckBoxState -PresetCheckBox $presetAITweaksBtn -PresetMap $script:AITweakPresetMap + } + finally { + $script:UpdatingTweakPresets = $false + } +} + +function Register-TweakPresetControlStateHandlers { + param([System.Windows.Window]$Window) + + if (-not $script:UiControlMappings) { return } + + foreach ($controlName in $script:UiControlMappings.Keys) { + $control = $Window.FindName($controlName) + if (-not $control) { continue } + + if ($control -is [System.Windows.Controls.CheckBox]) { + $control.Add_Checked({ if (-not $script:UpdatingTweakPresets) { Update-TweakPresetStates -Window $script:MainWindow } }) + $control.Add_Unchecked({ if (-not $script:UpdatingTweakPresets) { Update-TweakPresetStates -Window $script:MainWindow } }) + } + elseif ($control -is [System.Windows.Controls.ComboBox]) { + $control.Add_SelectionChanged({ if (-not $script:UpdatingTweakPresets) { Update-TweakPresetStates -Window $script:MainWindow } }) + } + } +} + +function Initialize-TweakPresetSources { + param( + [System.Windows.Window]$Window, + $DefaultSettingsJson, + $LastUsedSettingsJson + ) + + $script:DefaultTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $DefaultSettingsJson + $script:LastUsedTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $LastUsedSettingsJson + $script:PrivacyTweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'Privacy & Suggested Content' + $script:AITweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'AI' + + $presetLastUsedTweaksBtn = $Window.FindName('PresetLastUsedTweaksBtn') + if ($presetLastUsedTweaksBtn) { + $presetLastUsedTweaksBtn.Visibility = if ($script:LastUsedTweakPresetMap.Count -gt 0) { 'Visible' } else { 'Collapsed' } + } +} + +function Update-AppliedTweaksUserModeState { + param( + [System.Windows.Controls.CheckBox]$ShowCurrentlyAppliedTweaksCheckBox, + [System.Windows.Controls.ComboBox]$UserSelectionCombo + ) + + # Show/hide detect applied tweaks checkbox based on user mode + if ($ShowCurrentlyAppliedTweaksCheckBox) { + if ($UserSelectionCombo.SelectedIndex -eq 0) { + $ShowCurrentlyAppliedTweaksCheckBox.Visibility = 'Visible' + } + else { + $ShowCurrentlyAppliedTweaksCheckBox.Visibility = 'Collapsed' + } + } + + # Enable/disable user mode combo based on params only (not checkbox) + if ($script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')) { + $UserSelectionCombo.IsEnabled = $false + } + else { + $UserSelectionCombo.IsEnabled = $true + } +} + +function Update-UserSelectionDescription { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.ComboBox]$UserSelectionCombo, + [System.Windows.Controls.TextBox]$OtherUsernameTextBox, + [System.Windows.Controls.TextBlock]$UserSelectionDescription + ) + + switch ($UserSelectionCombo.SelectedIndex) { + 0 { + $currentUserName = GetUserName + if ([string]::IsNullOrWhiteSpace($currentUserName)) { + $UserSelectionDescription.Text = "The currently logged-in user profile" + } + else { + $UserSelectionDescription.Text = "The currently logged-in user profile: $currentUserName" + } + } + 1 { + $targetUserName = $OtherUsernameTextBox.Text.Trim() + if ([string]::IsNullOrWhiteSpace($targetUserName)) { + $UserSelectionDescription.Text = "A different user profile on this system" + } + else { + $UserSelectionDescription.Text = "A different user profile on this system: $targetUserName" + } + } + default { + $UserSelectionDescription.Text = "The default user template, affecting all new users created after this point. Useful for Sysprep deployment." + } + } +} + +function Test-OtherUsername { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.ComboBox]$UserSelectionCombo, + [System.Windows.Controls.TextBox]$OtherUsernameTextBox, + [System.Windows.Controls.TextBlock]$UsernameValidationMessage + ) + + # Only validate if "Other User" is selected + if ($UserSelectionCombo.SelectedIndex -ne 1) { + return $true + } + + $errorBrush = $Window.Resources['ValidationErrorColor'] + $successBrush = $Window.Resources['ValidationSuccessColor'] + $validationResult = Test-TargetUserName -UserName $OtherUsernameTextBox.Text + + $UsernameValidationMessage.Text = $validationResult.Message + if ($validationResult.IsValid) { + $UsernameValidationMessage.Foreground = $successBrush + return $true + } + + $UsernameValidationMessage.Foreground = $errorBrush + return $false +} diff --git a/Scripts/GUI/MainWindow-Navigation.ps1 b/Scripts/GUI/MainWindow-Navigation.ps1 new file mode 100644 index 0000000..29bcd8b --- /dev/null +++ b/Scripts/GUI/MainWindow-Navigation.ps1 @@ -0,0 +1,72 @@ +# MainWindow-Navigation.ps1 +# Wizard navigation helpers: tab navigation buttons and progress indicators. + +function Update-NavigationButtons { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.TabControl]$TabControl + ) + + $currentIndex = $TabControl.SelectedIndex + $totalTabs = $TabControl.Items.Count + + $previousBtn = $Window.FindName('PreviousBtn') + $nextBtn = $Window.FindName('NextBtn') + + $homeIndex = 0 + $overviewIndex = $totalTabs - 1 + + # Navigation button visibility + if ($currentIndex -eq $homeIndex) { + $nextBtn.Visibility = 'Collapsed' + $previousBtn.Visibility = 'Collapsed' + } + elseif ($currentIndex -eq $overviewIndex) { + $nextBtn.Visibility = 'Collapsed' + $previousBtn.Visibility = 'Visible' + } + else { + $nextBtn.Visibility = 'Visible' + $previousBtn.Visibility = 'Visible' + } + + # Update progress indicators + # Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Deployment Settings + $progressIndicator1 = $Window.FindName('ProgressIndicator1') # App Removal + $progressIndicator2 = $Window.FindName('ProgressIndicator2') # Tweaks + $progressIndicator3 = $Window.FindName('ProgressIndicator3') # Deployment Settings + $bottomNavGrid = $Window.FindName('BottomNavGrid') + + # Hide bottom navigation on home page + if ($currentIndex -eq 0) { + $bottomNavGrid.Visibility = 'Collapsed' + } + else { + $bottomNavGrid.Visibility = 'Visible' + } + + # Update indicator colors based on current tab + # Indicator 1 (App Removal) - tab index 1 + if ($currentIndex -ge 1) { + $progressIndicator1.Fill = $Window.Resources['ProgressActiveColor'] + } + else { + $progressIndicator1.Fill = $Window.Resources['ProgressInactiveColor'] + } + + # Indicator 2 (Tweaks) - tab index 2 + if ($currentIndex -ge 2) { + $progressIndicator2.Fill = $Window.Resources['ProgressActiveColor'] + } + else { + $progressIndicator2.Fill = $Window.Resources['ProgressInactiveColor'] + } + + # Indicator 3 (Deployment Settings) - tab index 3 + if ($currentIndex -ge 3) { + $progressIndicator3.Fill = $Window.Resources['ProgressActiveColor'] + } + else { + $progressIndicator3.Fill = $Window.Resources['ProgressInactiveColor'] + } +} diff --git a/Scripts/GUI/MainWindow-TweaksBuilder.ps1 b/Scripts/GUI/MainWindow-TweaksBuilder.ps1 new file mode 100644 index 0000000..62a904d --- /dev/null +++ b/Scripts/GUI/MainWindow-TweaksBuilder.ps1 @@ -0,0 +1,511 @@ +# MainWindow-TweaksBuilder.ps1 +# Dynamic tweaks UI construction from Features.json, tweak state management, selection clear, and search/highlight. + +function Build-DynamicTweaks { + param( + [System.Windows.Window]$Window, + [int]$WinVersion + ) + + $featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0" + + if (-not $featuresJson) { + throw "Unable to load Features.json file. The GUI cannot continue without feature definitions." + } + + # Column containers + $col0 = $Window.FindName('Column0Panel') + $col1 = $Window.FindName('Column1Panel') + $col2 = $Window.FindName('Column2Panel') + $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } + + # Clear all columns for fully dynamic panel creation + foreach ($col in $columns) { + if ($col) { $col.Children.Clear() } + } + + $script:UiControlMappings = @{} + $script:CategoryCardMap = @{} + $script:TweaksCompactMode = $null + $script:TweaksCardsMovedFromCol2 = @() + + function CreateLabeledCombo($parent, $labelText, $comboName, $items) { + # If only 2 items (No Change + one option), use a checkbox instead + if ($items.Count -eq 2) { + $checkbox = New-Object System.Windows.Controls.CheckBox + $checkbox.Content = $labelText + $checkbox.Name = $comboName + $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText) + $checkbox.IsChecked = $false + $checkbox.Style = $Window.Resources["FeatureCheckboxStyle"] + $parent.Children.Add($checkbox) | Out-Null + + # Register the checkbox with the window's name scope + try { + [System.Windows.NameScope]::SetNameScope($checkbox, [System.Windows.NameScope]::GetNameScope($Window)) + $Window.RegisterName($comboName, $checkbox) + } + catch { + # Name might already be registered, ignore + } + + return $checkbox + } + + # Otherwise use a combobox for multiple options + # Wrap label in a Border for search highlighting + $lblBorder = New-Object System.Windows.Controls.Border + $lblBorder.Style = $Window.Resources['LabelBorderStyle'] + $lblBorderName = "$comboName`_LabelBorder" + $lblBorder.Name = $lblBorderName + + $lbl = New-Object System.Windows.Controls.TextBlock + $lbl.Text = $labelText + $lbl.Style = $Window.Resources['LabelStyle'] + $labelName = "$comboName`_Label" + $lbl.Name = $labelName + + $lblBorder.Child = $lbl + $parent.Children.Add($lblBorder) | Out-Null + + # Register the label border with the window's name scope + try { + [System.Windows.NameScope]::SetNameScope($lblBorder, [System.Windows.NameScope]::GetNameScope($Window)) + $Window.RegisterName($lblBorderName, $lblBorder) + } + catch { + # Name might already be registered, ignore + } + + $combo = New-Object System.Windows.Controls.ComboBox + $combo.Name = $comboName + $combo.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText) + foreach ($item in $items) { $comboItem = New-Object System.Windows.Controls.ComboBoxItem; $comboItem.Content = $item; $combo.Items.Add($comboItem) | Out-Null } + $combo.SelectedIndex = 0 + $parent.Children.Add($combo) | Out-Null + + # Register the combo box with the window's name scope + try { + [System.Windows.NameScope]::SetNameScope($combo, [System.Windows.NameScope]::GetNameScope($Window)) + $Window.RegisterName($comboName, $combo) + } + catch { + # Name might already be registered, ignore + } + + return $combo + } + + function GetWikiUrlForCategory($category) { + if (-not $category) { return 'https://github.com/Raphire/Win11Debloat/wiki/Features' } + + $slug = $category.ToLowerInvariant() + $slug = $slug -replace '&', '' + $slug = $slug -replace '[^a-z0-9\s-]', '' + $slug = $slug -replace '\s', '-' + + return "https://github.com/Raphire/Win11Debloat/wiki/Features#$slug" + } + + function GetOrCreateCategoryCard($categoryObj) { + $categoryName = $categoryObj.Name + $categoryIcon = $categoryObj.Icon + + if ($script:CategoryCardMap.ContainsKey($categoryName)) { return $script:CategoryCardMap[$categoryName] } + + # Create a new card Border + StackPanel and add to shortest column + $target = $columns | Sort-Object @{Expression = { $_.Children.Count }; Ascending = $true }, @{Expression = { $columns.IndexOf($_) }; Ascending = $true } | Select-Object -First 1 + + $border = New-Object System.Windows.Controls.Border + $border.Style = $Window.Resources['CategoryCardBorderStyle'] + $border.Tag = 'DynamicCategory' + + $panel = New-Object System.Windows.Controls.StackPanel + $safe = ($categoryName -replace '[^a-zA-Z0-9_]', '_') + $panel.Name = "Category_{0}_Panel" -f $safe + + $headerRow = New-Object System.Windows.Controls.StackPanel + $headerRow.Orientation = 'Horizontal' + + # Add category icon + $icon = New-Object System.Windows.Controls.TextBlock + # Convert HTML entity to character (e.g.,  -> actual character) + if ($categoryIcon -match '&#x([0-9A-Fa-f]+);') { + $hexValue = [Convert]::ToInt32($matches[1], 16) + $icon.Text = [char]$hexValue + } + $icon.Style = $Window.Resources['CategoryHeaderIcon'] + $headerRow.Children.Add($icon) | Out-Null + + $header = New-Object System.Windows.Controls.TextBlock + $header.Text = $categoryName + $header.Style = $Window.Resources['CategoryHeaderTextBlock'] + $headerRow.Children.Add($header) | Out-Null + + $helpIcon = New-Object System.Windows.Controls.TextBlock + $helpIcon.Text = '(?)' + $helpIcon.Style = $Window.Resources['CategoryHelpLinkTextStyle'] + + $helpBtn = New-Object System.Windows.Controls.Button + $helpBtn.Content = $helpIcon + $helpBtn.ToolTip = "Open wiki for more info on '$categoryName' tweaks" + $helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName) + $helpBtn.Style = $Window.Resources['CategoryHelpLinkButtonStyle'] + $helpBtn.Add_Click({ + param($button, $e) + if ($button.Tag) { Start-Process $button.Tag } + }) + $headerRow.Children.Add($helpBtn) | Out-Null + + $panel.Children.Add($headerRow) | Out-Null + + $border.Child = $panel + $target.Children.Add($border) | Out-Null + + $script:CategoryCardMap[$categoryName] = $panel + return $panel + } + + # Determine categories present (from lists and features) + $categoriesPresent = @{} + if ($featuresJson.UiGroups) { + foreach ($g in $featuresJson.UiGroups) { if ($g.Category) { $categoriesPresent[$g.Category] = $true } } + } + foreach ($f in $featuresJson.Features) { if ($f.Category) { $categoriesPresent[$f.Category] = $true } } + + # Create cards in the order defined in Features.json Categories (if present) + $orderedCategories = @() + if ($featuresJson.Categories) { + foreach ($c in $featuresJson.Categories) { + $categoryName = if ($c -is [string]) { $c } else { $c.Name } + if ($categoriesPresent.ContainsKey($categoryName)) { + # Store the full category object (or create one with default icon for string categories) + $categoryObj = if ($c -is [string]) { @{Name = $c; Icon = '' } } else { $c } + $orderedCategories += $categoryObj + } + } + } + else { + # For backward compatibility, create category objects from keys + foreach ($catName in $categoriesPresent.Keys) { + $orderedCategories += @{Name = $catName; Icon = '' } + } + } + + foreach ($categoryObj in $orderedCategories) { + $categoryName = $categoryObj.Name + + # Create/get card for this category + $panel = GetOrCreateCategoryCard -categoryObj $categoryObj + if (-not $panel) { continue } + + # Collect groups and features for this category, then sort by priority + $categoryItems = @() + + # Add any groups for this category + if ($featuresJson.UiGroups) { + $groupIndex = 0 + foreach ($group in $featuresJson.UiGroups) { + if ($group.Category -ne $categoryName) { $groupIndex++; continue } + $categoryItems += [PSCustomObject]@{ + Type = 'group' + Data = $group + Priority = if ($null -ne $group.Priority) { $group.Priority } else { [int]::MaxValue } + OriginalIndex = $groupIndex + } + $groupIndex++ + } + } + + # Add individual features for this category + $featureIndex = 0 + foreach ($feature in $featuresJson.Features) { + if ($feature.Category -ne $categoryName) { $featureIndex++; continue } + + # Check version and feature compatibility using Features.json + if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) { + $featureIndex++; continue + } + + # Skip if feature part of a group + $inGroup = $false + if ($featuresJson.UiGroups) { + foreach ($g in $featuresJson.UiGroups) { foreach ($val in $g.Values) { if ($val.FeatureIds -contains $feature.FeatureId) { $inGroup = $true; break } }; if ($inGroup) { break } } + } + if ($inGroup) { $featureIndex++; continue } + + $categoryItems += [PSCustomObject]@{ + Type = 'feature' + Data = $feature + Priority = if ($null -ne $feature.Priority) { $feature.Priority } else { [int]::MaxValue } + OriginalIndex = $featureIndex + } + $featureIndex++ + } + + # Sort by priority first, then by original index for items with same/no priority + $sortedItems = $categoryItems | Sort-Object -Property Priority, OriginalIndex + + # Render sorted items + foreach ($item in $sortedItems) { + if ($item.Type -eq 'group') { + $group = $item.Data + $items = @('No Change') + ($group.Values | ForEach-Object { $_.Label }) + $comboName = 'Group_{0}Combo' -f $group.GroupId + $combo = CreateLabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items + # attach tooltip from UiGroups if present + if ($group.ToolTip) { + $tipBlock = New-Object System.Windows.Controls.TextBlock + $tipBlock.Text = $group.ToolTip + $tipBlock.TextWrapping = 'Wrap' + $tipBlock.MaxWidth = 420 + $combo.ToolTip = $tipBlock + $lblBorderObj = $null + try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {} + if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } + } + $script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $group.Values; Label = $group.Label; Category = $categoryName } + } + elseif ($item.Type -eq 'feature') { + $feature = $item.Data + $opt = 'Apply' + if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' } + $items = @('No Change', $opt) + $comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]', '' + $combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items + # attach tooltip from Features.json if present, and include the disabled-state reason + if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) { + $tooltipText = $feature.ToolTip + if ($feature.DisableWhenApplied -eq $true) { + $tooltipText = "This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change." + } + + $tipBlock = New-Object System.Windows.Controls.TextBlock + $tipBlock.Text = $tooltipText + $tipBlock.TextWrapping = 'Wrap' + $tipBlock.MaxWidth = 420 + $combo.ToolTip = $tipBlock + [System.Windows.Controls.ToolTipService]::SetShowOnDisabled($combo, $true) + $lblBorderObj = $null + try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {} + if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } + } + $script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; Category = $categoryName } + } + } + } + + # Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON + $script:FeatureLabelLookup = @{} + $script:UndoFeatureLabelLookup = @{} + foreach ($f in $featuresJson.Features) { + $script:FeatureLabelLookup[$f.FeatureId] = $f.Label + $script:UndoFeatureLabelLookup[$f.FeatureId] = $f.UndoLabel + } +} + +function Update-CurrentTweakSystemState { + param( + [System.Windows.Window]$Window, + [bool]$ApplyToUi + ) + + if (-not $script:UiControlMappings) { return } + if (-not $script:Features) { return } + + $featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0" + if (-not $featuresJson) { return } + + $groupMap = @{} + if ($featuresJson.UiGroups) { + foreach ($g in $featuresJson.UiGroups) { + $groupMap[$g.GroupId] = $g + } + } + + foreach ($controlName in $script:UiControlMappings.Keys) { + $control = $Window.FindName($controlName) + if (-not $control) { continue } + $mapping = $script:UiControlMappings[$controlName] + + if ($control -is [System.Windows.Controls.CheckBox] -and $mapping.Type -eq 'feature') { + $applied = $false + try { $applied = [bool](Test-FeatureApplied -FeatureId $mapping.FeatureId) } catch {} + $featureObj = $script:Features[$mapping.FeatureId] + $disableWhenApplied = $featureObj -and $featureObj.DisableWhenApplied -eq $true + Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemState' -Value $applied -Force + Add-Member -InputObject $control -MemberType NoteProperty -Name 'DisableWhenApplied' -Value $disableWhenApplied -Force + + if ($ApplyToUi) { + $control.IsChecked = $applied + $control.IsEnabled = -not ($applied -and $disableWhenApplied) + Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force + } + } + elseif ($control -is [System.Windows.Controls.ComboBox] -and $mapping.Type -eq 'group') { + $groupId = $null + if ($controlName -match '^Group_(.+)Combo$') { $groupId = $matches[1] } + $activeIndex = 0 + if ($groupId -and $groupMap.ContainsKey($groupId)) { + try { $activeIndex = Get-CurrentGroupActiveIndex -Group $groupMap[$groupId] } catch {} + } + Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemIndex' -Value $activeIndex -Force + + if ($ApplyToUi) { + $control.SelectedIndex = $activeIndex + Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $activeIndex -Force + } + } + } +} + +function Load-CurrentTweakStateIntoUI { + param([System.Windows.Window]$Window) + + Update-CurrentTweakSystemState -Window $Window -ApplyToUi:$true +} + +function Reset-TweaksToSystemState { + param( + [System.Windows.Window]$Window, + [bool]$LoadSystemState + ) + + if (-not $script:UiControlMappings) { return } + + foreach ($controlName in $script:UiControlMappings.Keys) { + $control = $Window.FindName($controlName) + if (-not $control) { continue } + + if ($control -is [System.Windows.Controls.CheckBox]) { + if ($LoadSystemState) { + # Set checkbox to the currently applied state from registry + $applied = if ($null -ne $control.PSObject.Properties['SystemState']) { [bool]$control.SystemState } else { $false } + $disableWhenApplied = $null -ne $control.PSObject.Properties['DisableWhenApplied'] -and [bool]$control.DisableWhenApplied + $control.IsChecked = $applied + $control.IsEnabled = -not ($applied -and $disableWhenApplied) + Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force + } + else { + # Clear the checkbox + $control.IsChecked = $false + $control.IsEnabled = $true + Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $false -Force + } + } + elseif ($control -is [System.Windows.Controls.ComboBox]) { + if ($LoadSystemState) { + # Set combobox to the currently applied state from registry + $idx = if ($null -ne $control.PSObject.Properties['SystemIndex']) { [int]$control.SystemIndex } else { 0 } + $control.SelectedIndex = $idx + Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $idx -Force + } + else { + # Reset to first item (No Change) + $control.SelectedIndex = 0 + Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value 0 -Force + } + } + } +} + +function Update-TweaksResponsiveColumns { + param([System.Windows.Window]$Window) + + $tweaksGrid = $Window.FindName('TweaksGrid') + $col0 = $Window.FindName('Column0Panel') + $col1 = $Window.FindName('Column1Panel') + $col2 = $Window.FindName('Column2Panel') + + if (-not $tweaksGrid -or -not $col0 -or -not $col1 -or -not $col2) { return } + if ($tweaksGrid.ColumnDefinitions.Count -lt 3) { return } + if ($null -eq $script:TweaksCardsMovedFromCol2) { $script:TweaksCardsMovedFromCol2 = @() } + + $useTwoColumns = $Window.ActualWidth -lt 1200 + if ($script:TweaksCompactMode -eq $useTwoColumns) { return } + $script:TweaksCompactMode = $useTwoColumns + + if ($useTwoColumns) { + $tweaksGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) + $tweaksGrid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) + $tweaksGrid.ColumnDefinitions[2].Width = [System.Windows.GridLength]::new(0) + $col2.Visibility = 'Collapsed' + + # Move third-column cards once when entering compact mode. + $cardsToMove = @($col2.Children) | Where-Object { $_ -is [System.Windows.UIElement] } + $script:TweaksCardsMovedFromCol2 = @($cardsToMove) + $col2.Children.Clear() + $targetColumns = @($col0, $col1) + foreach ($card in $cardsToMove) { + $target = $targetColumns | + Sort-Object @{Expression = { $_.Children.Count }; Ascending = $true }, @{Expression = { $targetColumns.IndexOf($_) }; Ascending = $true } | + Select-Object -First 1 + $target.Children.Add($card) | Out-Null + } + return + } + + $tweaksGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) + $tweaksGrid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) + $tweaksGrid.ColumnDefinitions[2].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) + $col2.Visibility = 'Visible' + + foreach ($card in (@($script:TweaksCardsMovedFromCol2) | Where-Object { $_ -is [System.Windows.UIElement] })) { + if ($col0.Children.Contains($card)) { + $col0.Children.Remove($card) | Out-Null + } + elseif ($col1.Children.Contains($card)) { + $col1.Children.Remove($card) | Out-Null + } + $col2.Children.Add($card) | Out-Null + } + $script:TweaksCardsMovedFromCol2 = @() +} + +function Clear-TweakSelections { + param([System.Windows.Window]$Window) + + if (-not $script:UiControlMappings) { return } + + foreach ($controlName in $script:UiControlMappings.Keys) { + $control = $Window.FindName($controlName) + if ($control -is [System.Windows.Controls.CheckBox]) { + $control.IsChecked = $false + $control.IsEnabled = $true + } + elseif ($control -is [System.Windows.Controls.ComboBox]) { + $control.SelectedIndex = 0 + } + } +} + +function Clear-TweakHighlights { + param([System.Windows.Window]$Window) + + $col0 = $Window.FindName('Column0Panel') + $col1 = $Window.FindName('Column1Panel') + $col2 = $Window.FindName('Column2Panel') + $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } + foreach ($column in $columns) { + foreach ($card in $column.Children) { + if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) { + foreach ($control in $card.Child.Children) { + if ($control -is [System.Windows.Controls.CheckBox] -or + ($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder')) { + $control.Background = [System.Windows.Media.Brushes]::Transparent + } + } + } + } + } +} + +function Test-ComboBoxContainsMatch { + param ([System.Windows.Controls.ComboBox]$ComboBox, [string]$SearchText) + + foreach ($item in $ComboBox.Items) { + $itemText = if ($item -is [System.Windows.Controls.ComboBoxItem]) { $item.Content.ToString().ToLower() } else { $item.ToString().ToLower() } + if ($itemText.Contains($SearchText)) { return $true } + } + return $false +} diff --git a/Scripts/GUI/MainWindow-WindowChrome.ps1 b/Scripts/GUI/MainWindow-WindowChrome.ps1 new file mode 100644 index 0000000..810fb71 --- /dev/null +++ b/Scripts/GUI/MainWindow-WindowChrome.ps1 @@ -0,0 +1,215 @@ +# MainWindow-WindowChrome.ps1 +# Window sizing, DPI-aware coordinate conversion, maximized-window taskbar-constraint helpers, and UI animations. + +function Register-MaximizedWindowHelper { + if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) { + Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper ` + -ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' ` + -MemberDefinition @' + [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + private struct MINMAXINFO { + public POINT ptReserved, ptMaxSize, ptMaxPosition, ptMinTrackSize, ptMaxTrackSize; + } + [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + private struct POINT { public int x, y; } + + [System.Runtime.InteropServices.DllImport("user32.dll")] + private static extern System.IntPtr MonitorFromWindow(System.IntPtr hwnd, uint dwFlags); + + [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)] + private static extern bool GetMonitorInfo(System.IntPtr hMonitor, ref MONITORINFO lpmi); + + [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + private struct RECT { + public int Left, Top, Right, Bottom; + } + + [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Auto)] + private struct MONITORINFO { + public int cbSize; + public RECT rcMonitor; + public RECT rcWork; + public uint dwFlags; + } + + public static System.IntPtr WmGetMinMaxInfoHook( + System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) { + if (msg == 0x0024) { // WM_GETMINMAXINFO + var mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure( + lParam, typeof(MINMAXINFO)); + + const uint MONITOR_DEFAULTTONEAREST = 0x00000002; + var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + var monitorInfo = new MONITORINFO(); + monitorInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MONITORINFO)); + + if (monitor != System.IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo)) { + mmi.ptMaxPosition.x = monitorInfo.rcWork.Left - monitorInfo.rcMonitor.Left; + mmi.ptMaxPosition.y = monitorInfo.rcWork.Top - monitorInfo.rcMonitor.Top; + mmi.ptMaxSize.x = monitorInfo.rcWork.Right - monitorInfo.rcWork.Left; + mmi.ptMaxSize.y = monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top; + } + else { + var screen = System.Windows.Forms.Screen.FromHandle(hwnd); + var wa = screen.WorkingArea; + var bounds = screen.Bounds; + mmi.ptMaxPosition.x = wa.Left - bounds.Left; + mmi.ptMaxPosition.y = wa.Top - bounds.Top; + mmi.ptMaxSize.x = wa.Width; + mmi.ptMaxSize.y = wa.Height; + } + + System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true); + } + return System.IntPtr.Zero; + } +'@ + } +} + +# Convert screen-pixel coordinates to WPF device-independent pixels (DIP) +function ConvertTo-ScreenPointToDip { + param( + [System.Windows.Window]$Window, + [double]$X, + [double]$Y + ) + + $source = [System.Windows.PresentationSource]::FromVisual($Window) + if ($null -eq $source -or $null -eq $source.CompositionTarget) { + return [System.Windows.Point]::new($X, $Y) + } + + return $source.CompositionTarget.TransformFromDevice.Transform([System.Windows.Point]::new($X, $Y)) +} + +# Convert screen-pixel size to WPF device-independent size +function ConvertTo-ScreenPixelsToDip { + param( + [System.Windows.Window]$Window, + [double]$Width, + [double]$Height + ) + + $topLeft = ConvertTo-ScreenPointToDip -Window $Window -X 0 -Y 0 + $bottomRight = ConvertTo-ScreenPointToDip -Window $Window -X $Width -Y $Height + return [System.Windows.Size]::new($bottomRight.X - $topLeft.X, $bottomRight.Y - $topLeft.Y) +} + +# Get the screen that currently contains the window +function Get-WindowScreen { + param([System.Windows.Window]$Window) + + $hwnd = (New-Object System.Windows.Interop.WindowInteropHelper($Window)).Handle + if ($hwnd -eq [IntPtr]::Zero) { + return $null + } + + return [System.Windows.Forms.Screen]::FromHandle($hwnd) +} + +# Update window border/corner chrome when transitioning between Normal and Maximized +function Update-MainWindowChrome { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.Border]$MainBorder, + [System.Windows.Controls.Border]$TitleBarBackground, + [object]$NormalWindowShadow + ) + + $windowStateMaximized = [System.Windows.WindowState]::Maximized + $chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($Window) + + if ($Window.WindowState -eq $windowStateMaximized) { + $MainBorder.Margin = [System.Windows.Thickness]::new(0) + $MainBorder.BorderThickness = [System.Windows.Thickness]::new(0) + $MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0) + $MainBorder.Effect = $null + $TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(0) + # Zero out resize borders when maximized so the entire title bar row is draggable + if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(0) } + } + else { + $MainBorder.Margin = [System.Windows.Thickness]::new(0) + $MainBorder.BorderThickness = [System.Windows.Thickness]::new(1) + $MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(8) + $MainBorder.Effect = $NormalWindowShadow + $TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(8, 8, 0, 0) + if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(5) } + } +} + +# Set the initial window size and center on screen (normal state only) +function Set-MainWindowInitialSize { + param( + [System.Windows.Window]$Window, + [double]$InitialNormalMaxWidth = 1400.0 + ) + + if ($Window.WindowState -ne [System.Windows.WindowState]::Normal) { + return + } + + $screen = Get-WindowScreen -Window $Window + if ($null -eq $screen) { + return + } + + $workingAreaTopLeftDip = ConvertTo-ScreenPointToDip -Window $Window -X $screen.WorkingArea.Left -Y $screen.WorkingArea.Top + $workingAreaDip = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.WorkingArea.Width -Height $screen.WorkingArea.Height + $Window.Width = [Math]::Min($InitialNormalMaxWidth, $workingAreaDip.Width) + $Window.Left = $workingAreaTopLeftDip.X + (($workingAreaDip.Width - $Window.Width) / 2) +} + +# Update the content grid margin to constrain max content width +function Update-MainWindowContentMargin { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.Grid]$ContentGrid, + [double]$MaxContentWidth = 1600.0 + ) + + $w = $Window.ActualWidth + if ($w -gt $MaxContentWidth) { + $gutter = [Math]::Floor(($w - $MaxContentWidth) / 2) + $ContentGrid.Margin = [System.Windows.Thickness]::new($gutter, 0, $gutter, 0) + } + else { + $ContentGrid.Margin = [System.Windows.Thickness]::new(0) + } +} + +# Vertically center the home content panel +function Update-MainWindowHomeContentPosition { + param( + [System.Windows.Window]$Window, + [System.Windows.Controls.Panel]$HomeContentPanel + ) + + if ($HomeContentPanel) { + $availableHeight = $Window.ActualHeight - 32 # subtract title bar height + if ($availableHeight -gt 0) { + $topMargin = ($availableHeight - 584) * 0.5 + $HomeContentPanel.Margin = [System.Windows.Thickness]::new(0, $topMargin, 0, 0) + } + } +} + +function Start-DropdownArrowAnimation { + param( + [System.Windows.Controls.TextBlock]$Arrow, + [double]$Angle + ) + + if (-not $Arrow) { return } + + $animation = New-Object System.Windows.Media.Animation.DoubleAnimation + $animation.To = $Angle + $animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200)) + + $ease = New-Object System.Windows.Media.Animation.CubicEase + $ease.EasingMode = 'EaseOut' + $animation.EasingFunction = $ease + + $Arrow.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation) +} diff --git a/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1 b/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1 index f2143e6..7bbda18 100644 --- a/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1 +++ b/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1 @@ -79,16 +79,70 @@ function Test-RestoreDialogFeatureVisibleInOverview { return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category) } +function Get-SelectedForwardFeatureIdsFromBackup { + param($SelectedBackup) + + $selectedFeatureIds = New-Object System.Collections.Generic.List[string] + $seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) + + foreach ($featureId in @($SelectedBackup.SelectedFeatures)) { + if ([string]::IsNullOrWhiteSpace([string]$featureId)) { + continue + } + + $normalizedId = [string]$featureId + if ($seenSelectedFeatureIds.Add($normalizedId)) { + $selectedFeatureIds.Add($normalizedId) + } + } + + return @($selectedFeatureIds.ToArray()) +} + +function Get-SelectedUndoFeatureIdsFromBackup { + param($SelectedBackup) + + $selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string] + $seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) + + foreach ($featureId in @($SelectedBackup.SelectedUndoFeatures)) { + if ([string]::IsNullOrWhiteSpace([string]$featureId)) { + continue + } + + $normalizedId = [string]$featureId + if ($seenUndoFeatureIds.Add($normalizedId)) { + $selectedUndoFeatureIds.Add($normalizedId) + } + } + + return @($selectedUndoFeatureIds.ToArray()) +} + +function Get-CombinedSelectedFeatureIdsFromBackup { + param($SelectedBackup) + + $featureIds = New-Object System.Collections.Generic.List[string] + $seenIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) + + foreach ($featureId in @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup) + @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)) { + if ([string]::IsNullOrWhiteSpace([string]$featureId)) { + continue + } + + $normalizedId = [string]$featureId + if ($seenIds.Add($normalizedId)) { + $featureIds.Add($normalizedId) + } + } + + return @($featureIds.ToArray()) +} + function Get-SelectedFeatureIdsFromBackup { param($SelectedBackup) - return @( - foreach ($featureId in @($SelectedBackup.SelectedFeatures)) { - if (-not [string]::IsNullOrWhiteSpace([string]$featureId)) { - [string]$featureId - } - } - ) + return @(Get-CombinedSelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup) } function Get-RestoreBackupFeatureLists { diff --git a/Scripts/GUI/Show-ApplyModal.ps1 b/Scripts/GUI/Show-ApplyModal.ps1 index 30bc6ca..d9ccfb5 100644 --- a/Scripts/GUI/Show-ApplyModal.ps1 +++ b/Scripts/GUI/Show-ApplyModal.ps1 @@ -186,7 +186,7 @@ function Show-ApplyModal { $applyRebootPanel.Visibility = 'Visible' } else { - $script:ApplyCompletionMessageEl.Text = "Your clean system is ready. Thanks for using Win11Debloat!" + $script:ApplyCompletionMessageEl.Text = "Your system is ready. Thanks for using Win11Debloat!" } } } diff --git a/Scripts/GUI/Show-MainWindow.ps1 b/Scripts/GUI/Show-MainWindow.ps1 index 9209123..d4771ee 100644 --- a/Scripts/GUI/Show-MainWindow.ps1 +++ b/Scripts/GUI/Show-MainWindow.ps1 @@ -1,8 +1,7 @@ -function Show-MainWindow { +function Show-MainWindow { Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null - # Helper to constrain the maximized window to the monitor working area (respects taskbar). - # Required for WindowStyle=None windows — without this the window extends behind the taskbar. + # ---- Constrain maximized window to taskbar work area ---- if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) { Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper ` -ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' ` @@ -67,12 +66,10 @@ function Show-MainWindow { '@ } - # Get current Windows build version $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild - $usesDarkMode = GetSystemUsesDarkMode - # Load XAML from file + # ---- Load XAML ---- $xaml = Get-Content -Path $script:MainWindowSchema -Raw $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) try { @@ -84,7 +81,6 @@ function Show-MainWindow { SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode - # Get named elements $mainBorder = $window.FindName('MainBorder') $titleBarBackground = $window.FindName('TitleBarBackground') $kofiBtn = $window.FindName('KofiBtn') @@ -97,88 +93,37 @@ function Show-MainWindow { $importConfigBtn = $window.FindName('ImportConfigBtn') $exportConfigBtn = $window.FindName('ExportConfigBtn') $restoreBackupBtn = $window.FindName('RestoreBackupBtn') + $homeContentPanel = $window.FindName('HomeContentPanel') + $contentGrid = $window.FindName('ContentGrid') + $maxContentWidth = 1600.0 $windowStateNormal = [System.Windows.WindowState]::Normal $windowStateMaximized = [System.Windows.WindowState]::Maximized $normalWindowShadow = $mainBorder.Effect $initialNormalMaxWidth = 1400.0 - $convertScreenPointToDip = { - param( - [double]$x, - [double]$y - ) + $script:MainWindow = $window + $script:GuiWindow = $window - $source = [System.Windows.PresentationSource]::FromVisual($window) - if ($null -eq $source -or $null -eq $source.CompositionTarget) { - return [System.Windows.Point]::new($x, $y) - } + # ---- Handle unhandled exceptions on the dispatcher thread ---- + [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Add_UnhandledException({ + param($sender, $e) + Write-Warning "Unhandled exception in GUI: $($e.Exception.Message)" + Write-Warning "Stack trace: $($e.Exception.StackTrace)" + $e.Handled = $true + }) - return $source.CompositionTarget.TransformFromDevice.Transform([System.Windows.Point]::new($x, $y)) - } - - $convertScreenPixelsToDip = { - param( - [double]$width, - [double]$height - ) - - $topLeft = & $convertScreenPointToDip 0 0 - $bottomRight = & $convertScreenPointToDip $width $height - return [System.Windows.Size]::new($bottomRight.X - $topLeft.X, $bottomRight.Y - $topLeft.Y) - } - - $getWindowScreen = { - $hwnd = (New-Object System.Windows.Interop.WindowInteropHelper($window)).Handle - if ($hwnd -eq [IntPtr]::Zero) { - return $null - } - - return [System.Windows.Forms.Screen]::FromHandle($hwnd) - } - - $updateWindowChrome = { - $chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($window) - if ($window.WindowState -eq $windowStateMaximized) { - $mainBorder.Margin = [System.Windows.Thickness]::new(0) - $mainBorder.BorderThickness = [System.Windows.Thickness]::new(0) - $mainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0) - $mainBorder.Effect = $null - $titleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(0) - # Zero out resize borders when maximized so the entire title bar row is draggable - if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(0) } - } - else { - $mainBorder.Margin = [System.Windows.Thickness]::new(0) - $mainBorder.BorderThickness = [System.Windows.Thickness]::new(1) - $mainBorder.CornerRadius = [System.Windows.CornerRadius]::new(8) - $mainBorder.Effect = $normalWindowShadow - $titleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(8, 8, 0, 0) - if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(5) } - } - } - - $applyInitialWindowSize = { - if ($window.WindowState -ne $windowStateNormal) { - return - } - - $screen = & $getWindowScreen - if ($null -eq $screen) { - return - } - - $workingAreaTopLeftDip = & $convertScreenPointToDip $screen.WorkingArea.Left $screen.WorkingArea.Top - $workingAreaDip = & $convertScreenPixelsToDip $screen.WorkingArea.Width $screen.WorkingArea.Height - $window.Width = [Math]::Min($initialNormalMaxWidth, $workingAreaDip.Width) - $window.Left = $workingAreaTopLeftDip.X + (($workingAreaDip.Width - $window.Width) / 2) - } + # ---- Window chrome helpers ---- + $updateWindowChrome = { Update-MainWindowChrome -Window $window -MainBorder $mainBorder -TitleBarBackground $titleBarBackground -NormalWindowShadow $normalWindowShadow } + $applyInitialWindowSize = { Set-MainWindowInitialSize -Window $window -InitialNormalMaxWidth $initialNormalMaxWidth } + $updateContentMargin = { Update-MainWindowContentMargin -Window $window -ContentGrid $contentGrid -MaxContentWidth $maxContentWidth } + $updateHomeContentPosition = { Update-MainWindowHomeContentPosition -Window $window -HomeContentPanel $homeContentPanel } + # ---- Window chrome event wiring ---- $window.Add_SourceInitialized({ & $applyInitialWindowSize & $updateWindowChrome - # Register WM_GETMINMAXINFO hook so maximizing respects the working area (taskbar) $hwndHelper = New-Object System.Windows.Interop.WindowInteropHelper($window) $hwndSource = [System.Windows.Interop.HwndSource]::FromHwnd($hwndHelper.Handle) $hookMethod = [Win11Debloat.MaximizedWindowHelper].GetMethod('WmGetMinMaxInfoHook') @@ -186,53 +131,32 @@ function Show-MainWindow { $hwndSource.AddHook($hook) }) - $contentGrid = $window.FindName('ContentGrid') - $maxContentWidth = 1600.0 - - $updateContentMargin = { - $w = $window.ActualWidth - if ($w -gt $maxContentWidth) { - $gutter = [Math]::Floor(($w - $maxContentWidth) / 2) - $contentGrid.Margin = [System.Windows.Thickness]::new($gutter, 0, $gutter, 0) - } else { - $contentGrid.Margin = [System.Windows.Thickness]::new(0) - } - } - $window.Add_SizeChanged({ & $updateContentMargin - UpdateTweaksResponsiveColumns + & $updateHomeContentPosition + Update-TweaksResponsiveColumns -Window $window }) - $window.Add_StateChanged({ - & $updateWindowChrome - }) + $window.Add_StateChanged({ & $updateWindowChrome }) $window.Add_LocationChanged({ - # Nudge the popup offset to force WPF to recalculate its screen position if ($script:BubblePopup -and $script:BubblePopup.IsOpen) { $script:BubblePopup.HorizontalOffset += 1 $script:BubblePopup.HorizontalOffset -= 1 } }) - - $kofiBtn.Add_Click({ - Start-Process "https://ko-fi.com/raphire" - }) - + + # ---- Menu/button event wiring ---- + $kofiBtn.Add_Click({ Start-Process "https://ko-fi.com/raphire" }) + $menuBtn.Add_Click({ $menuBtn.ContextMenu.PlacementTarget = $menuBtn $menuBtn.ContextMenu.Placement = [System.Windows.Controls.Primitives.PlacementMode]::Bottom $menuBtn.ContextMenu.IsOpen = $true }) - $menuDocumentation.Add_Click({ - Start-Process "https://github.com/Raphire/Win11Debloat/wiki" - }) - - $menuReportBug.Add_Click({ - Start-Process "https://github.com/Raphire/Win11Debloat/issues" - }) + $menuDocumentation.Add_Click({ Start-Process "https://github.com/Raphire/Win11Debloat/wiki" }) + $menuReportBug.Add_Click({ Start-Process "https://github.com/Raphire/Win11Debloat/issues" }) $menuLogs.Add_Click({ $logsFolder = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'Logs' @@ -244,59 +168,12 @@ function Show-MainWindow { } }) - $menuAbout.Add_Click({ - Show-AboutDialog -Owner $window - }) + $menuAbout.Add_Click({ Show-AboutDialog -Owner $window }) - $exportConfigBtn.Add_Click({ - try { - Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox - } - catch { - Write-Warning "Export configuration failed: $($_.Exception.Message)" - Show-MessageBox -Owner $window -Message "Unable to open export configuration dialog: $($_.Exception.Message)" -Title 'Export Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null - } - }) + $closeBtn.Add_Click({ $window.Close() }) + $window.Add_Closing({ $script:CancelRequested = $true }) - $importConfigBtn.Add_Click({ - try { - Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { UpdateAppSelectionStatus; UpdatePresetStates } -OnImportCompleted { - $tabControl.SelectedIndex = 3 - UpdateNavigationButtons - - $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{ - Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here' - }) | Out-Null - } - } - catch { - Write-Warning "Import configuration failed: $($_.Exception.Message)" - Show-MessageBox -Owner $window -Message "Unable to open import configuration dialog: $($_.Exception.Message)" -Title 'Import Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null - } - }) - - if ($restoreBackupBtn) { - $restoreBackupBtn.Add_Click({ - try { - Show-RestoreBackupWindow -Owner $window - } - catch { - Write-Warning "Restore backup action failed: $($_.Exception.Message)" - Show-MessageBox -Owner $window -Message "Unable to open restore backup dialog: $($_.Exception.Message)" -Title 'Restore Backup Failed' -Button 'OK' -Icon 'Error' | Out-Null - } - }) - } - - $closeBtn.Add_Click({ - $window.Close() - }) - - # Ensure closing the main window stops all execution - $window.Add_Closing({ - $script:CancelRequested = $true - }) - - # Integrated App Selection UI + # ---- App Selection panel elements ---- $appsPanel = $window.FindName('AppSelectionPanel') $onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox') $loadingAppsIndicator = $window.FindName('LoadingAppsIndicator') @@ -322,95 +199,101 @@ function Show-MainWindow { $presetAITweaksBtn = $window.FindName('PresetAITweaksBtn') $tweaksPresetsArrow = $window.FindName('TweaksPresetsArrow') - function AttachTriStateClickBehavior { - param([System.Windows.Controls.CheckBox]$checkBox) + # ---- Navigation elements ---- + $tabControl = $window.FindName('MainTabControl') + $previousBtn = $window.FindName('PreviousBtn') + $nextBtn = $window.FindName('NextBtn') + $userSelectionCombo = $window.FindName('UserSelectionCombo') + $userSelectionDescription = $window.FindName('UserSelectionDescription') + $otherUserPanel = $window.FindName('OtherUserPanel') + $otherUsernameTextBox = $window.FindName('OtherUsernameTextBox') + $usernameTextBoxPlaceholder = $window.FindName('UsernameTextBoxPlaceholder') + $usernameValidationMessage = $window.FindName('UsernameValidationMessage') + $appRemovalScopeCombo = $window.FindName('AppRemovalScopeCombo') + $appRemovalScopeDescription = $window.FindName('AppRemovalScopeDescription') + $appRemovalScopeSection = $window.FindName('AppRemovalScopeSection') + $appRemovalScopeCurrentUser = $window.FindName('AppRemovalScopeCurrentUser') + $appRemovalScopeTargetUser = $window.FindName('AppRemovalScopeTargetUser') - if (-not $checkBox -or -not $checkBox.IsThreeState) { return } + # ---- Tweak search elements ---- + $tweakSearchBox = $window.FindName('TweakSearchBox') + $tweakSearchPlaceholder = $window.FindName('TweakSearchPlaceholder') + $tweakSearchBorder = $window.FindName('TweakSearchBorder') + $tweaksScrollViewer = $window.FindName('TweaksScrollViewer') + $tweaksGrid = $window.FindName('TweaksGrid') + $ShowCurrentlyAppliedTweaksCheckBox = $window.FindName('ShowCurrentlyAppliedTweaksCheckBox') + $clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn') - if (-not $checkBox.PSObject.Properties['WasIndeterminateBeforeClick']) { - Add-Member -InputObject $checkBox -MemberType NoteProperty -Name 'WasIndeterminateBeforeClick' -Value $false + # ---- Deployment elements ---- + $reviewChangesBtn = $window.FindName('ReviewChangesBtn') + $deploymentApplyBtn = $window.FindName('DeploymentApplyBtn') + $homeStartBtn = $window.FindName('HomeStartBtn') + $homeDefaultModeBtn = $window.FindName('HomeDefaultModeBtn') + + # ---- Wire export/import ---- + $exportConfigBtn.Add_Click({ + try { + Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox } + catch { + Write-Warning "Export configuration failed: $($_.Exception.Message)" + Show-MessageBox -Owner $window -Message "Unable to open export configuration dialog: $($_.Exception.Message)" -Title 'Export Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null + } + }) - $checkBox.Add_PreviewMouseLeftButtonDown({ - $this.WasIndeterminateBeforeClick = ($this.IsChecked -eq [System.Nullable[bool]]$null) + $importConfigBtn.Add_Click({ + try { + Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { Update-AppSelectionStatus -AppsPanel $appsPanel -AppSelectionStatus $appSelectionStatus -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection -AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo; Update-AppPresetStates -AppsPanel $appsPanel } -OnImportCompleted { + $tabControl.SelectedIndex = 3 + Update-NavigationButtons -Window $window -TabControl $tabControl + $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{ + Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here' + }) | Out-Null + } + } + catch { + Write-Warning "Import configuration failed: $($_.Exception.Message)" + Show-MessageBox -Owner $window -Message "Unable to open import configuration dialog: $($_.Exception.Message)" -Title 'Import Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null + } + }) + + # ---- Restore backup ---- + if ($restoreBackupBtn) { + $restoreBackupBtn.Add_Click({ + try { + $restoreResult = Show-RestoreBackupWindow -Owner $window + if ($restoreResult -and $restoreResult.RestoredRegistry -eq $true) { + Update-CurrentTweakSystemState -Window $window -ApplyToUi:$false + + if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) { + Reset-TweaksToSystemState -Window $window -LoadSystemState $true + Update-TweakPresetStates -Window $window + } + } + } + catch { + Write-Warning "Restore backup action failed: $($_.Exception.Message)" + Show-MessageBox -Owner $window -Message "Unable to open restore backup dialog: $($_.Exception.Message)" -Title 'Restore Backup Failed' -Button 'OK' -Icon 'Error' | Out-Null + } }) } - function NormalizeCheckboxState { - param([System.Windows.Controls.CheckBox]$checkBox) - - if ($checkBox.PSObject.Properties['WasIndeterminateBeforeClick'] -and $checkBox.WasIndeterminateBeforeClick) { - # WPF toggles null -> false before Click handlers fire; restore desired mixed -> checked behavior. - $checkBox.WasIndeterminateBeforeClick = $false - $checkBox.IsChecked = $true - return $true - } - - return ($checkBox.IsChecked -eq $true) - } - - function SetTriStatePresetCheckBoxState { - param( - [System.Windows.Controls.CheckBox]$CheckBox, - [int]$Total, - [int]$Selected - ) - - if (-not $CheckBox) { return } - - if ($Total -eq 0) { - $CheckBox.IsEnabled = $false - $CheckBox.IsChecked = $false - return - } - - $CheckBox.IsEnabled = $true - if ($Selected -eq 0) { - $CheckBox.IsChecked = $false - } - elseif ($Selected -eq $Total) { - $CheckBox.IsChecked = $true - } - else { - $CheckBox.IsChecked = [System.Nullable[bool]]$null - } - } - - function ClearTweakSelections { - if (-not $script:UiControlMappings) { return } - - foreach ($controlName in $script:UiControlMappings.Keys) { - $control = $window.FindName($controlName) - if ($control -is [System.Windows.Controls.CheckBox]) { - $control.IsChecked = $false - } - elseif ($control -is [System.Windows.Controls.ComboBox]) { - $control.SelectedIndex = 0 - } - } - } - - function AnimateDropdownArrow { - param( - [System.Windows.Controls.TextBlock]$arrow, - [double]$angle - ) - - if (-not $arrow) { return } - - $animation = New-Object System.Windows.Media.Animation.DoubleAnimation - $animation.To = $angle - $animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200)) - - $ease = New-Object System.Windows.Media.Animation.CubicEase - $ease.EasingMode = 'EaseOut' - $animation.EasingFunction = $ease - - $arrow.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation) - } - - # Load JSON-defined presets and build dynamic preset checkboxes + # ---- Script-level state initialization ---- + $script:MainWindowLastSelectedCheckbox = $null + $script:IsLoadingApps = $false + $script:PendingDefaultMode = $false + $script:PreloadedAppData = $null + $script:UpdatingPresets = $false + $script:UpdatingTweakPresets = $false + $script:SortColumn = 'Name' + $script:SortAscending = $true + $script:AppSearchMatches = @() + $script:AppSearchMatchIndex = -1 $script:JsonPresetCheckboxes = @() + + if ($importConfigBtn) { $importConfigBtn.IsEnabled = $false } + + # ---- Build JSON-defined app presets ---- foreach ($preset in (LoadAppPresetsFromJson)) { $checkbox = New-Object System.Windows.Controls.CheckBox $checkbox.Content = $preset.Name @@ -418,7 +301,7 @@ function Show-MainWindow { $checkbox.Style = $window.Resources['PresetCheckBoxStyle'] $checkbox.ToolTip = "Select $($preset.Name)" $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $preset.Name) - AttachTriStateClickBehavior -checkBox $checkbox + Add-TriStateClickBehavior -CheckBox $checkbox Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'PresetAppIds' -Value $preset.AppIds $jsonPresetsPanel.Children.Add($checkbox) | Out-Null $script:JsonPresetCheckboxes += $checkbox @@ -426,687 +309,92 @@ function Show-MainWindow { $checkbox.Add_Click({ if ($script:UpdatingPresets) { return } $presetIds = $this.PresetAppIds - $check = NormalizeCheckboxState -checkBox $this - ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $presetIds -contains $_ }).Count -gt 0 }.GetNewClosure() -Check $check + $check = ConvertTo-NormalizedCheckboxState -CheckBox $this + Invoke-AppPreset -AppsPanel $appsPanel -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $presetIds -contains $_ }).Count -gt 0 }.GetNewClosure() -Check $check }) } - - # Track the last selected checkbox for shift-click range selection - $script:MainWindowLastSelectedCheckbox = $null - - # Guard flag: true while a load is in progress; prevents concurrent loads - $script:IsLoadingApps = $false - # Flag set when Default Mode is clicked before apps have finished loading - $script:PendingDefaultMode = $false - # Holds apps data preloaded before ShowDialog() so the first load skips the background job - $script:PreloadedAppData = $null - # Prevent app import until the apps list has finished initial population. - if ($importConfigBtn) { - $importConfigBtn.IsEnabled = $false - } - - # Set script-level variable for GUI window reference - $script:GuiWindow = $window - - # Guard flag to prevent preset handlers from firing when we update their state programmatically - $script:UpdatingPresets = $false - $script:UpdatingTweakPresets = $false - - # Sort state for the app table - $script:SortColumn = 'Name' - $script:SortAscending = $true - - function UpdateSortArrows { - $ease = New-Object System.Windows.Media.Animation.CubicEase - $ease.EasingMode = 'EaseOut' - $arrows = @{ - 'Name' = $sortArrowName - 'Description' = $sortArrowDescription - 'AppId' = $sortArrowAppId - } - foreach ($col in $arrows.Keys) { - $tb = $arrows[$col] - # Active column: full opacity, rotate to indicate direction (0 = up/asc, 180 = down/desc) - # Inactive columns: dim, reset to 0 - if ($col -eq $script:SortColumn) { - $targetAngle = if ($script:SortAscending) { 0 } else { 180 } - $tb.Opacity = 1.0 - } else { - $targetAngle = 0 - $tb.Opacity = 0.3 - } - $anim = New-Object System.Windows.Media.Animation.DoubleAnimation - $anim.To = $targetAngle - $anim.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200)) - $anim.EasingFunction = $ease - $tb.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $anim) - } + # ---- App sort helpers ---- + $updateSortArrows = { + Update-SortArrows ` + -SortArrowName $sortArrowName -SortArrowDescription $sortArrowDescription -SortArrowAppId $sortArrowAppId } - # Rebuilds $script:AppSearchMatches by scanning appsPanel children in their current order, - # collecting any that are still highlighted. Preserves the active match across reorderings. - function RebuildAppSearchIndex { + $rebuildAppSearchIndex = { param($activeMatch = $null) - $newMatches = @() - $newActiveIndex = -1 - $i = 0 - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.Background -ne [System.Windows.Media.Brushes]::Transparent) { - $newMatches += $child - if ($null -ne $activeMatch -and [System.Object]::ReferenceEquals($child, $activeMatch)) { - $newActiveIndex = $i - } - $i++ - } - } - $script:AppSearchMatches = $newMatches - $script:AppSearchMatchIndex = if ($newActiveIndex -ge 0) { $newActiveIndex } elseif ($newMatches.Count -gt 0) { 0 } else { -1 } + Update-AppsPanelRebuildSearchIndex -AppsPanel $appsPanel -ActiveMatch $activeMatch } - function SortApps { - $children = @($appsPanel.Children) - $key = switch ($script:SortColumn) { - 'Name' { { $_.AppName } } - 'Description' { { $_.AppDescription } } - 'AppId' { { $_.AppIdDisplay } } - } - $sorted = $children | Sort-Object $key -Descending:(-not $script:SortAscending) - $appsPanel.Children.Clear() - foreach ($checkbox in $sorted) { - $appsPanel.Children.Add($checkbox) | Out-Null - } - UpdateSortArrows - - # Rebuild search match list in new sorted order so keyboard navigation stays correct + $sortApps = { + Update-AppsPanelSort -AppsPanel $appsPanel ` + -SortArrowName $sortArrowName -SortArrowDescription $sortArrowDescription -SortArrowAppId $sortArrowAppId if ($script:AppSearchMatches.Count -gt 0) { $activeMatch = if ($script:AppSearchMatchIndex -ge 0 -and $script:AppSearchMatchIndex -lt $script:AppSearchMatches.Count) { $script:AppSearchMatches[$script:AppSearchMatchIndex] } else { $null } - RebuildAppSearchIndex -activeMatch $activeMatch + & $rebuildAppSearchIndex -activeMatch $activeMatch } } - function SetSortColumn($column) { + $setSortColumn = { + param($column) if ($script:SortColumn -eq $column) { $script:SortAscending = -not $script:SortAscending - } else { + } + else { $script:SortColumn = $column $script:SortAscending = $true } - SortApps + & $sortApps } - function UpdateAppSelectionStatus { - $selectedCount = 0 - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { - $selectedCount++ - } - } - $appSelectionStatus.Text = "$selectedCount app(s) selected for removal" - - if ($appRemovalScopeCombo -and $appRemovalScopeSection -and $appRemovalScopeDescription) { - if ($selectedCount -gt 0) { - if ($userSelectionCombo.SelectedIndex -ne 2) { - $appRemovalScopeCombo.IsEnabled = $true - } - UpdateAppRemovalScopeDescription - } - else { - $appRemovalScopeCombo.IsEnabled = $false - $appRemovalScopeDescription.Text = "No apps selected for removal." - } - } + # ---- Tri-state preset wiring for app presets ---- + foreach ($presetCheckBox in @($presetDefaultApps, $presetLastUsed)) { + Add-TriStateClickBehavior -CheckBox $presetCheckBox } - # Applies a preset by checking/unchecking apps that match the given filter - # When -Exclusive is set, all apps are unchecked first so only matching apps end up selected - function ApplyPresetToApps { - param ( - [scriptblock]$MatchFilter, - [bool]$Check, - [switch]$Exclusive - ) - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox]) { - if ($Exclusive) { - $child.IsChecked = (& $MatchFilter $child) - } elseif (& $MatchFilter $child) { - $child.IsChecked = $Check - } - } - } - UpdatePresetStates - } - - # Update preset checkboxes to reflect checked/indeterminate/unchecked state - function UpdatePresetStates { - $script:UpdatingPresets = $true - try { - # Helper: count matching and checked apps, set checkbox state - function SetPresetState($checkbox, [scriptblock]$MatchFilter) { - $total = 0; $checked = 0 - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox]) { - if (& $MatchFilter $child) { - $total++ - if ($child.IsChecked) { $checked++ } - } - } - } - SetTriStatePresetCheckBoxState -CheckBox $checkbox -Total $total -Selected $checked - } - - SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true } - foreach ($jsonCb in $script:JsonPresetCheckboxes) { - $localIds = $jsonCb.PresetAppIds - SetPresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure() - } - - # Last used preset: only update if it's visible (has saved apps) - if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) { - SetPresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } - } - } - finally { - $script:UpdatingPresets = $false - } - } - - # Dynamically builds Tweaks UI from Features.json - function BuildDynamicTweaks { - $featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0" - - if (-not $featuresJson) { - Show-MessageBox -Message "Unable to load Features.json file!" -Title "Error" -Button 'OK' -Icon 'Error' | Out-Null - Exit - } - - # Column containers - $col0 = $window.FindName('Column0Panel') - $col1 = $window.FindName('Column1Panel') - $col2 = $window.FindName('Column2Panel') - $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } - - # Clear all columns for fully dynamic panel creation - foreach ($col in $columns) { - if ($col) { $col.Children.Clear() } - } - - $script:UiControlMappings = @{} - $script:CategoryCardMap = @{} - $script:TweaksCompactMode = $null - $script:TweaksCardsMovedFromCol2 = @() - - function CreateLabeledCombo($parent, $labelText, $comboName, $items) { - # If only 2 items (No Change + one option), use a checkbox instead - if ($items.Count -eq 2) { - $checkbox = New-Object System.Windows.Controls.CheckBox - $checkbox.Content = $labelText - $checkbox.Name = $comboName - $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText) - $checkbox.IsChecked = $false - $checkbox.Style = $window.Resources["FeatureCheckboxStyle"] - $parent.Children.Add($checkbox) | Out-Null - - # Register the checkbox with the window's name scope - try { - [System.Windows.NameScope]::SetNameScope($checkbox, [System.Windows.NameScope]::GetNameScope($window)) - $window.RegisterName($comboName, $checkbox) - } - catch { - # Name might already be registered, ignore - } - - return $checkbox - } - - # Otherwise use a combobox for multiple options - # Wrap label in a Border for search highlighting - $lblBorder = New-Object System.Windows.Controls.Border - $lblBorder.Style = $window.Resources['LabelBorderStyle'] - $lblBorderName = "$comboName`_LabelBorder" - $lblBorder.Name = $lblBorderName - - $lbl = New-Object System.Windows.Controls.TextBlock - $lbl.Text = $labelText - $lbl.Style = $window.Resources['LabelStyle'] - $labelName = "$comboName`_Label" - $lbl.Name = $labelName - - $lblBorder.Child = $lbl - $parent.Children.Add($lblBorder) | Out-Null - - # Register the label border with the window's name scope - try { - [System.Windows.NameScope]::SetNameScope($lblBorder, [System.Windows.NameScope]::GetNameScope($window)) - $window.RegisterName($lblBorderName, $lblBorder) - } - catch { - # Name might already be registered, ignore - } - - $combo = New-Object System.Windows.Controls.ComboBox - $combo.Name = $comboName - $combo.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText) - foreach ($item in $items) { $comboItem = New-Object System.Windows.Controls.ComboBoxItem; $comboItem.Content = $item; $combo.Items.Add($comboItem) | Out-Null } - $combo.SelectedIndex = 0 - $parent.Children.Add($combo) | Out-Null - - # Register the combo box with the window's name scope - try { - [System.Windows.NameScope]::SetNameScope($combo, [System.Windows.NameScope]::GetNameScope($window)) - $window.RegisterName($comboName, $combo) - } - catch { - # Name might already be registered, ignore - } - - return $combo - } - - function GetWikiUrlForCategory($category) { - if (-not $category) { return 'https://github.com/Raphire/Win11Debloat/wiki/Features' } - - $slug = $category.ToLowerInvariant() - $slug = $slug -replace '&', '' - $slug = $slug -replace '[^a-z0-9\s-]', '' - $slug = $slug -replace '\s', '-' - - return "https://github.com/Raphire/Win11Debloat/wiki/Features#$slug" - } - - function GetOrCreateCategoryCard($categoryObj) { - $categoryName = $categoryObj.Name - $categoryIcon = $categoryObj.Icon - - if ($script:CategoryCardMap.ContainsKey($categoryName)) { return $script:CategoryCardMap[$categoryName] } - - # Create a new card Border + StackPanel and add to shortest column - $target = $columns | Sort-Object @{Expression={$_.Children.Count}; Ascending=$true}, @{Expression={$columns.IndexOf($_)}; Ascending=$true} | Select-Object -First 1 - - $border = New-Object System.Windows.Controls.Border - $border.Style = $window.Resources['CategoryCardBorderStyle'] - $border.Tag = 'DynamicCategory' - - $panel = New-Object System.Windows.Controls.StackPanel - $safe = ($categoryName -replace '[^a-zA-Z0-9_]','_') - $panel.Name = "Category_{0}_Panel" -f $safe - - $headerRow = New-Object System.Windows.Controls.StackPanel - $headerRow.Orientation = 'Horizontal' - - # Add category icon - $icon = New-Object System.Windows.Controls.TextBlock - # Convert HTML entity to character (e.g.,  -> actual character) - if ($categoryIcon -match '&#x([0-9A-Fa-f]+);') { - $hexValue = [Convert]::ToInt32($matches[1], 16) - $icon.Text = [char]$hexValue - } - $icon.Style = $window.Resources['CategoryHeaderIcon'] - $headerRow.Children.Add($icon) | Out-Null - - $header = New-Object System.Windows.Controls.TextBlock - $header.Text = $categoryName - $header.Style = $window.Resources['CategoryHeaderTextBlock'] - $headerRow.Children.Add($header) | Out-Null - - $helpIcon = New-Object System.Windows.Controls.TextBlock - $helpIcon.Text = '(?)' - $helpIcon.Style = $window.Resources['CategoryHelpLinkTextStyle'] - - $helpBtn = New-Object System.Windows.Controls.Button - $helpBtn.Content = $helpIcon - $helpBtn.ToolTip = "Open wiki for more info on '$categoryName' tweaks" - $helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName) - $helpBtn.Style = $window.Resources['CategoryHelpLinkButtonStyle'] - $helpBtn.Add_Click({ - param($button, $e) - if ($button.Tag) { Start-Process $button.Tag } - }) - $headerRow.Children.Add($helpBtn) | Out-Null - - $panel.Children.Add($headerRow) | Out-Null - - $border.Child = $panel - $target.Children.Add($border) | Out-Null - - $script:CategoryCardMap[$categoryName] = $panel - return $panel - } - - # Determine categories present (from lists and features) - $categoriesPresent = @{} - if ($featuresJson.UiGroups) { - foreach ($g in $featuresJson.UiGroups) { if ($g.Category) { $categoriesPresent[$g.Category] = $true } } - } - foreach ($f in $featuresJson.Features) { if ($f.Category) { $categoriesPresent[$f.Category] = $true } } - - # Create cards in the order defined in Features.json Categories (if present) - $orderedCategories = @() - if ($featuresJson.Categories) { - foreach ($c in $featuresJson.Categories) { - $categoryName = if ($c -is [string]) { $c } else { $c.Name } - if ($categoriesPresent.ContainsKey($categoryName)) { - # Store the full category object (or create one with default icon for string categories) - $categoryObj = if ($c -is [string]) { @{Name = $c; Icon = ''} } else { $c } - $orderedCategories += $categoryObj - } - } - } else { - # For backward compatibility, create category objects from keys - foreach ($catName in $categoriesPresent.Keys) { - $orderedCategories += @{Name = $catName; Icon = ''} - } - } - - foreach ($categoryObj in $orderedCategories) { - $categoryName = $categoryObj.Name - - # Create/get card for this category - $panel = GetOrCreateCategoryCard -categoryObj $categoryObj - if (-not $panel) { continue } - - # Collect groups and features for this category, then sort by priority - $categoryItems = @() - - # Add any groups for this category - if ($featuresJson.UiGroups) { - $groupIndex = 0 - foreach ($group in $featuresJson.UiGroups) { - if ($group.Category -ne $categoryName) { $groupIndex++; continue } - $categoryItems += [PSCustomObject]@{ - Type = 'group' - Data = $group - Priority = if ($null -ne $group.Priority) { $group.Priority } else { [int]::MaxValue } - OriginalIndex = $groupIndex - } - $groupIndex++ - } - } - - # Add individual features for this category - $featureIndex = 0 - foreach ($feature in $featuresJson.Features) { - if ($feature.Category -ne $categoryName) { $featureIndex++; continue } - - # Check version and feature compatibility using Features.json - if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) { - $featureIndex++; continue - } - - # Skip if feature part of a group - $inGroup = $false - if ($featuresJson.UiGroups) { - foreach ($g in $featuresJson.UiGroups) { foreach ($val in $g.Values) { if ($val.FeatureIds -contains $feature.FeatureId) { $inGroup = $true; break } }; if ($inGroup) { break } } - } - if ($inGroup) { $featureIndex++; continue } - - $categoryItems += [PSCustomObject]@{ - Type = 'feature' - Data = $feature - Priority = if ($null -ne $feature.Priority) { $feature.Priority } else { [int]::MaxValue } - OriginalIndex = $featureIndex - } - $featureIndex++ - } - - # Sort by priority first, then by original index for items with same/no priority - $sortedItems = $categoryItems | Sort-Object -Property Priority, OriginalIndex - - # Render sorted items - foreach ($item in $sortedItems) { - if ($item.Type -eq 'group') { - $group = $item.Data - $items = @('No Change') + ($group.Values | ForEach-Object { $_.Label }) - $comboName = 'Group_{0}Combo' -f $group.GroupId - $combo = CreateLabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items - # attach tooltip from UiGroups if present - if ($group.ToolTip) { - $tipBlock = New-Object System.Windows.Controls.TextBlock - $tipBlock.Text = $group.ToolTip - $tipBlock.TextWrapping = 'Wrap' - $tipBlock.MaxWidth = 420 - $combo.ToolTip = $tipBlock - $lblBorderObj = $null - try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {} - if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } - } - $script:UiControlMappings[$comboName] = @{ Type='group'; Values = $group.Values; Label = $group.Label; Category = $categoryName } - } - elseif ($item.Type -eq 'feature') { - $feature = $item.Data - $opt = 'Apply' - if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' } - $items = @('No Change', $opt) - $comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]','' - $combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items - # attach tooltip from Features.json if present - if ($feature.ToolTip) { - $tipBlock = New-Object System.Windows.Controls.TextBlock - $tipBlock.Text = $feature.ToolTip - $tipBlock.TextWrapping = 'Wrap' - $tipBlock.MaxWidth = 420 - $combo.ToolTip = $tipBlock - $lblBorderObj = $null - try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {} - if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } - } - $script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; Category = $categoryName } - } - } - } - - # Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON - $script:FeatureLabelLookup = @{} - foreach ($f in $featuresJson.Features) { - $script:FeatureLabelLookup[$f.FeatureId] = $f.Label - } - } - - # Helper function to load apps and populate the app list panel - function script:LoadAppsWithList($listOfApps) { - $script:MainWindowLastSelectedCheckbox = $null - - $loaderScriptPath = $script:LoadAppsDetailsScriptPath - $appsFilePath = $script:AppsListFilePath - $onlyInstalled = [bool]$onlyInstalledAppsBox.IsChecked - - # Use preloaded data if available; otherwise load in background job - if (-not $onlyInstalled -and $script:PreloadedAppData) { - $rawAppData = $script:PreloadedAppData - $script:PreloadedAppData = $null - } else { - # Load apps details in a background job to keep the UI responsive - $rawAppData = Invoke-NonBlocking -ScriptBlock { - param($loaderScript, $appsListFilePath, $installedList, $onlyInstalled) - $script:AppsListFilePath = $appsListFilePath - . $loaderScript - LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false - } -ArgumentList $loaderScriptPath, $appsFilePath, $listOfApps, $onlyInstalled - } - - $appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName) - - $loadingAppsIndicator.Visibility = 'Collapsed' - - if ($appsToAdd.Count -eq 0) { - $window.FindName('DeploymentApplyBtn').IsEnabled = $true - if ($importConfigBtn) { - $importConfigBtn.IsEnabled = $true - } - return - } - - $brushSafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#4CAF50') - $brushUnsafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#F44336') - $brushDefault = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#FFC107') - $brushSafe.Freeze(); $brushUnsafe.Freeze(); $brushDefault.Freeze() - - # Create WPF controls; pump the Dispatcher every batch so the spinner keeps animating. - $batchSize = 20 - for ($i = 0; $i -lt $appsToAdd.Count; $i++) { - $app = $appsToAdd[$i] - - $checkbox = New-Object System.Windows.Controls.CheckBox - $automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppIdDisplay) { $app.AppIdDisplay } else { $null } - if ($automationName) { $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $automationName) } - $checkbox.Tag = $app.AppIdDisplay - $checkbox.IsChecked = $app.IsChecked - $checkbox.Style = $window.Resources['AppsPanelCheckBoxStyle'] - - # Build table row: Recommendation dot | Name | Description | App ID - $row = New-Object System.Windows.Controls.Grid - $row.Style = $window.Resources['AppTableRowStyle'] - $c0 = New-Object System.Windows.Controls.ColumnDefinition; $c0.Width = $window.Resources['AppTableDotColWidth'] - $c1 = New-Object System.Windows.Controls.ColumnDefinition; $c1.Width = $window.Resources['AppTableNameColWidth'] - $c2 = New-Object System.Windows.Controls.ColumnDefinition; $c2.Width = $window.Resources['AppTableDescColWidth'] - $c3 = New-Object System.Windows.Controls.ColumnDefinition; $c3.Width = $window.Resources['AppTableIdColWidth'] - $row.ColumnDefinitions.Add($c0); $row.ColumnDefinitions.Add($c1) - $row.ColumnDefinitions.Add($c2); $row.ColumnDefinitions.Add($c3) - - $dot = New-Object System.Windows.Shapes.Ellipse - $dot.Style = $window.Resources['AppRecommendationDotStyle'] - $dot.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } } - $dot.ToolTip = switch ($app.Recommendation) { - 'safe' { '[Recommended] Safe to remove for most users' } - 'unsafe' { '[Not Recommended] Only remove if you know what you are doing' } - default { "[Optional] Remove if you don't need this app" } - } - [System.Windows.Controls.Grid]::SetColumn($dot, 0) - - $tbName = New-Object System.Windows.Controls.TextBlock - $tbName.Text = $app.FriendlyName - $tbName.Style = $window.Resources['AppNameTextStyle'] - [System.Windows.Controls.Grid]::SetColumn($tbName, 1) - - $tbDesc = New-Object System.Windows.Controls.TextBlock - $tbDesc.Text = $app.Description - $tbDesc.Style = $window.Resources['AppDescTextStyle'] - $tbDesc.ToolTip = $app.Description - [System.Windows.Controls.Grid]::SetColumn($tbDesc, 2) - - $tbId = New-Object System.Windows.Controls.TextBlock - $tbId.Text = $app.AppIdDisplay - $tbId.Style = $window.Resources["AppIdTextStyle"] - $tbId.ToolTip = $app.AppIdDisplay - [System.Windows.Controls.Grid]::SetColumn($tbId, 3) - - $row.Children.Add($dot) | Out-Null - $row.Children.Add($tbName) | Out-Null - $row.Children.Add($tbDesc) | Out-Null - $row.Children.Add($tbId) | Out-Null - $checkbox.Content = $row - - Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppName' -Value $app.FriendlyName - Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppDescription' -Value $app.Description - Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'SelectedByDefault' -Value $app.SelectedByDefault - Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($app.AppId) - Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIdDisplay' -Value $app.AppIdDisplay - - $checkbox.Add_Checked({ UpdateAppSelectionStatus }) - $checkbox.Add_Unchecked({ UpdateAppSelectionStatus }) - AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel ` - -lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) ` - -updateStatusCallback { UpdateAppSelectionStatus } - - $appsPanel.Children.Add($checkbox) | Out-Null - - if (($i + 1) % $batchSize -eq 0) { DoEvents } - } - - SortApps - - # If Default Mode was clicked while apps were still loading, apply defaults now - if ($script:PendingDefaultMode) { - $script:PendingDefaultMode = $false - ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive - } - - UpdateAppSelectionStatus - - # Re-enable Apply button now that the full, correctly-checked app list is ready - $window.FindName('DeploymentApplyBtn').IsEnabled = $true - if ($importConfigBtn) { - $importConfigBtn.IsEnabled = $true - } - } - - # Loads apps into the UI - function LoadAppsIntoMainUI { - # Prevent concurrent loads - if ($script:IsLoadingApps) { return } - $script:IsLoadingApps = $true - - if ($importConfigBtn) { - $importConfigBtn.IsEnabled = $false - } - - # Show loading indicator and clear existing apps - $loadingAppsIndicator.Visibility = 'Visible' - $appsPanel.Children.Clear() - - # Disable Apply button while apps are loading so it can't be clicked with a partial list - $window.FindName('DeploymentApplyBtn').IsEnabled = $false - - # Update navigation buttons to disable Next/Previous - UpdateNavigationButtons - - # Force a render so the loading indicator is visible, then schedule the - # actual loading at Background priority so this call returns immediately. - # This is critical when called from Add_Loaded: the window must finish - # its initialization before we start a nested message pump via DoEvents. - $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{}) - $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ - try { - $listOfApps = "" - - if ($onlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) { - $listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking - - if ($null -eq $listOfApps) { - Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null - $onlyInstalledAppsBox.IsChecked = $false - } - } - - LoadAppsWithList $listOfApps - } - finally { - $script:IsLoadingApps = $false - } - }) | Out-Null - } - - # Event handlers for app selection - $onlyInstalledAppsBox.Add_Checked({ - LoadAppsIntoMainUI - }) - $onlyInstalledAppsBox.Add_Unchecked({ - LoadAppsIntoMainUI + # ---- Preset: Default selection ---- + $presetDefaultApps.Add_Click({ + if ($script:UpdatingPresets) { return } + $check = ConvertTo-NormalizedCheckboxState -CheckBox $this + Invoke-AppPreset -AppsPanel $appsPanel -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Check $check }) - # Animate arrow when popup opens/closes, and lazily update preset states + # ---- Clear selection button ---- + $clearAppSelectionBtn.Add_Click({ + Invoke-AppPreset -AppsPanel $appsPanel -MatchFilter { param($c) $true } -Check $false + }) + + # ---- Column header sort handlers ---- + $headerNameBtn.Add_MouseLeftButtonUp({ & $setSortColumn 'Name' }) + $headerDescriptionBtn.Add_MouseLeftButtonUp({ & $setSortColumn 'Description' }) + $headerAppIdBtn.Add_MouseLeftButtonUp({ & $setSortColumn 'AppId' }) + + # ---- Load apps ---- + $appLoadStatusCallback = { Update-AppSelectionStatus -AppsPanel $appsPanel -AppSelectionStatus $appSelectionStatus -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection -AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo } + $onlyInstalledAppsBox.Add_Checked({ Load-AppsIntoMainUI -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn }) + $onlyInstalledAppsBox.Add_Unchecked({ Load-AppsIntoMainUI -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn }) + + # ---- App presets popup ---- $presetsPopup.Add_Opened({ - UpdatePresetStates - AnimateDropdownArrow -arrow $presetsArrow -angle 180 + Update-AppPresetStates -AppsPanel $appsPanel + Start-DropdownArrowAnimation -Arrow $presetsArrow -Angle 180 }) $presetsPopup.Add_Closed({ - AnimateDropdownArrow -arrow $presetsArrow -angle 0 + Start-DropdownArrowAnimation -Arrow $presetsArrow -Angle 0 $presetsBtn.IsChecked = $false }) $tweaksPresetsPopup.Add_Opened({ - UpdateTweakPresetStates - AnimateDropdownArrow -arrow $tweaksPresetsArrow -angle 180 + Update-TweakPresetStates -Window $window + Start-DropdownArrowAnimation -Arrow $tweaksPresetsArrow -Angle 180 }) $tweaksPresetsPopup.Add_Closed({ - AnimateDropdownArrow -arrow $tweaksPresetsArrow -angle 0 + Start-DropdownArrowAnimation -Arrow $tweaksPresetsArrow -Angle 0 $tweaksPresetsBtn.IsChecked = $false }) - # Close popup when clicking anywhere outside the popup or the presets button. + # ---- Popup dismiss on outside click ---- $window.Add_PreviewMouseDown({ $isAppPopupOpen = $presetsPopup.IsOpen $isTweaksPopupOpen = $tweaksPresetsPopup.IsOpen @@ -1125,13 +413,12 @@ function Show-MainWindow { } }) - # Close the preset menu when the main window loses focus (e.g., user switches to another app). $window.Add_Deactivated({ if ($presetsPopup.IsOpen) { $presetsPopup.IsOpen = $false } if ($tweaksPresetsPopup.IsOpen) { $tweaksPresetsPopup.IsOpen = $false } }) - # Toggle popup on button click + # ---- Toggle popup on button click ---- $presetsBtn.Add_Click({ $presetsPopup.IsOpen = -not $presetsPopup.IsOpen $presetsBtn.IsChecked = $presetsPopup.IsOpen @@ -1142,113 +429,28 @@ function Show-MainWindow { $tweaksPresetsBtn.IsChecked = $tweaksPresetsPopup.IsOpen }) - foreach ($presetCheckBox in @( - $presetDefaultApps, - $presetLastUsed, - $presetDefaultTweaksBtn, - $presetLastUsedTweaksBtn, - $presetPrivacyTweaksBtn, - $presetAITweaksBtn - )) { - AttachTriStateClickBehavior -checkBox $presetCheckBox - } - - # Preset: Default selection - $presetDefaultApps.Add_Click({ - if ($script:UpdatingPresets) { return } - $check = NormalizeCheckboxState -checkBox $this - ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Check $check - }) - - # Clear selection button + reset all preset checkboxes - $clearAppSelectionBtn.Add_Click({ - ApplyPresetToApps -MatchFilter { param($c) $true } -Check $false - }) - - # Column header sort handlers - $headerNameBtn.Add_MouseLeftButtonUp({ SetSortColumn 'Name' }) - $headerDescriptionBtn.Add_MouseLeftButtonUp({ SetSortColumn 'Description' }) - $headerAppIdBtn.Add_MouseLeftButtonUp({ SetSortColumn 'AppId' }) - - # Helper function to scroll to an item if it's not visible, centering it in the viewport - function ScrollToItemIfNotVisible { - param ( - [System.Windows.Controls.ScrollViewer]$scrollViewer, - [System.Windows.UIElement]$item, - [System.Windows.UIElement]$container - ) - - if (-not $scrollViewer -or -not $item -or -not $container) { return } - - try { - $itemPosition = $item.TransformToAncestor($container).Transform([System.Windows.Point]::new(0, 0)).Y - $viewportHeight = $scrollViewer.ViewportHeight - $itemHeight = $item.ActualHeight - $currentOffset = $scrollViewer.VerticalOffset - - # Check if the item is currently visible in the viewport - $itemTop = $itemPosition - $currentOffset - $itemBottom = $itemTop + $itemHeight - - $isVisible = ($itemTop -ge 0) -and ($itemBottom -le $viewportHeight) - - # Only scroll if the item is not visible - if (-not $isVisible) { - # Center the item in the viewport - $targetOffset = $itemPosition - ($viewportHeight / 2) + ($itemHeight / 2) - $scrollViewer.ScrollToVerticalOffset([Math]::Max(0, $targetOffset)) - } - } - catch { - # Fallback to simple bring into view - $item.BringIntoView() - } - } - - # Helper function to find the parent ScrollViewer of an element - function FindParentScrollViewer { - param ([System.Windows.UIElement]$element) - - $parent = [System.Windows.Media.VisualTreeHelper]::GetParent($element) - while ($null -ne $parent) { - if ($parent -is [System.Windows.Controls.ScrollViewer]) { - return $parent - } - $parent = [System.Windows.Media.VisualTreeHelper]::GetParent($parent) - } - return $null - } - - # App Search Box functionality + # ---- App Search Box ---- $appSearchBox = $window.FindName('AppSearchBox') $appSearchPlaceholder = $window.FindName('AppSearchPlaceholder') - - # Track current search matches and active index for Enter-key navigation - $script:AppSearchMatches = @() - $script:AppSearchMatchIndex = -1 - + $appSearchBox.Add_TextChanged({ $searchText = $appSearchBox.Text.ToLower().Trim() - - # Show/hide placeholder $appSearchPlaceholder.Visibility = if ([string]::IsNullOrWhiteSpace($appSearchBox.Text)) { 'Visible' } else { 'Collapsed' } - - # Clear all highlights first + foreach ($child in $appsPanel.Children) { if ($child -is [System.Windows.Controls.CheckBox]) { $child.Background = [System.Windows.Media.Brushes]::Transparent } } - + $script:AppSearchMatches = @() $script:AppSearchMatchIndex = -1 - + if ([string]::IsNullOrWhiteSpace($searchText)) { return } - - # Find and highlight all matching apps + $highlightBrush = $window.Resources["SearchHighlightColor"] $activeHighlightBrush = $window.Resources["SearchHighlightActiveColor"] - + foreach ($child in $appsPanel.Children) { if ($child -is [System.Windows.Controls.CheckBox] -and $child.Visibility -eq 'Visible') { $appName = if ($child.AppName) { $child.AppName } else { '' } @@ -1260,145 +462,56 @@ function Show-MainWindow { } } } - - # Scroll to first match and mark it as active + if ($script:AppSearchMatches.Count -gt 0) { $script:AppSearchMatchIndex = 0 $script:AppSearchMatches[0].Background = $activeHighlightBrush - $scrollViewer = FindParentScrollViewer -element $appsPanel + $scrollViewer = Find-ParentScrollViewer -Element $appsPanel if ($scrollViewer) { - ScrollToItemIfNotVisible -scrollViewer $scrollViewer -item $script:AppSearchMatches[0] -container $appsPanel + Scroll-ToItemIfNotVisible -ScrollViewer $scrollViewer -Item $script:AppSearchMatches[0] -Container $appsPanel } } }) - + $appSearchBox.Add_KeyDown({ param($sourceControl, $e) if ($e.Key -eq [System.Windows.Input.Key]::Enter -and $script:AppSearchMatches.Count -gt 0) { - # Reset background of current active match $script:AppSearchMatches[$script:AppSearchMatchIndex].Background = $window.Resources["SearchHighlightColor"] - # Advance to next match (wrapping) $script:AppSearchMatchIndex = ($script:AppSearchMatchIndex + 1) % $script:AppSearchMatches.Count - # Highlight new active match $script:AppSearchMatches[$script:AppSearchMatchIndex].Background = $window.Resources["SearchHighlightActiveColor"] - $scrollViewer = FindParentScrollViewer -element $appsPanel + $scrollViewer = Find-ParentScrollViewer -Element $appsPanel if ($scrollViewer) { - ScrollToItemIfNotVisible -scrollViewer $scrollViewer -item $script:AppSearchMatches[$script:AppSearchMatchIndex] -container $appsPanel + Scroll-ToItemIfNotVisible -ScrollViewer $scrollViewer -Item $script:AppSearchMatches[$script:AppSearchMatchIndex] -Container $appsPanel } $e.Handled = $true } }) - # Tweak Search Box functionality - $tweakSearchBox = $window.FindName('TweakSearchBox') - $tweakSearchPlaceholder = $window.FindName('TweakSearchPlaceholder') - $tweakSearchBorder = $window.FindName('TweakSearchBorder') - $tweaksScrollViewer = $window.FindName('TweaksScrollViewer') - $tweaksGrid = $window.FindName('TweaksGrid') - $col0 = $window.FindName('Column0Panel') - $col1 = $window.FindName('Column1Panel') - $col2 = $window.FindName('Column2Panel') - - function UpdateTweaksResponsiveColumns { - if (-not $tweaksGrid -or -not $col0 -or -not $col1 -or -not $col2) { return } - if ($tweaksGrid.ColumnDefinitions.Count -lt 3) { return } - if ($null -eq $script:TweaksCardsMovedFromCol2) { $script:TweaksCardsMovedFromCol2 = @() } - - $useTwoColumns = $window.ActualWidth -lt 1200 - if ($script:TweaksCompactMode -eq $useTwoColumns) { return } - $script:TweaksCompactMode = $useTwoColumns - - if ($useTwoColumns) { - $tweaksGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) - $tweaksGrid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) - $tweaksGrid.ColumnDefinitions[2].Width = [System.Windows.GridLength]::new(0) - $col2.Visibility = 'Collapsed' - - # Move third-column cards once when entering compact mode. - $cardsToMove = @($col2.Children) | Where-Object { $_ -is [System.Windows.UIElement] } - $script:TweaksCardsMovedFromCol2 = @($cardsToMove) - $col2.Children.Clear() - $targetColumns = @($col0, $col1) - foreach ($card in $cardsToMove) { - $target = $targetColumns | - Sort-Object @{Expression={$_.Children.Count}; Ascending=$true}, @{Expression={$targetColumns.IndexOf($_)}; Ascending=$true} | - Select-Object -First 1 - $target.Children.Add($card) | Out-Null - } - return - } - - $tweaksGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) - $tweaksGrid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) - $tweaksGrid.ColumnDefinitions[2].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) - $col2.Visibility = 'Visible' - - foreach ($card in (@($script:TweaksCardsMovedFromCol2) | Where-Object { $_ -is [System.Windows.UIElement] })) { - if ($col0.Children.Contains($card)) { - $col0.Children.Remove($card) | Out-Null - } - elseif ($col1.Children.Contains($card)) { - $col1.Children.Remove($card) | Out-Null - } - $col2.Children.Add($card) | Out-Null - } - $script:TweaksCardsMovedFromCol2 = @() - } - - # Monitor scrollbar visibility and adjust searchbar margin + # ---- Tweak Search Box ---- $tweaksScrollViewer.Add_ScrollChanged({ if ($tweaksScrollViewer.ScrollableHeight -gt 0) { - # The 17px accounts for the scrollbar width + some padding $tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0, 0, 17, 0) - } else { + } + else { $tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0) } }) - - # Helper function to clear all tweak highlights - function ClearTweakHighlights { - $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } - foreach ($column in $columns) { - foreach ($card in $column.Children) { - if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) { - foreach ($control in $card.Child.Children) { - if ($control -is [System.Windows.Controls.CheckBox] -or - ($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder')) { - $control.Background = [System.Windows.Media.Brushes]::Transparent - } - } - } - } - } - } - - # Helper function to check if a ComboBox contains matching items - function ComboBoxContainsMatch { - param ([System.Windows.Controls.ComboBox]$comboBox, [string]$searchText) - - foreach ($item in $comboBox.Items) { - $itemText = if ($item -is [System.Windows.Controls.ComboBoxItem]) { $item.Content.ToString().ToLower() } else { $item.ToString().ToLower() } - if ($itemText.Contains($searchText)) { return $true } - } - return $false - } - + $tweakSearchBox.Add_TextChanged({ $searchText = $tweakSearchBox.Text.ToLower().Trim() - - # Show/hide placeholder $tweakSearchPlaceholder.Visibility = if ([string]::IsNullOrWhiteSpace($tweakSearchBox.Text)) { 'Visible' } else { 'Collapsed' } - - # Clear all highlights - ClearTweakHighlights - + + Clear-TweakHighlights -Window $window + if ([string]::IsNullOrWhiteSpace($searchText)) { return } - - # Find and highlight all matching tweaks + $firstMatch = $null $highlightBrush = $window.Resources["SearchHighlightColor"] + $col0 = $window.FindName('Column0Panel') + $col1 = $window.FindName('Column1Panel') + $col2 = $window.FindName('Column2Panel') $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } - + foreach ($column in $columns) { foreach ($card in $column.Children) { if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) { @@ -1407,7 +520,7 @@ function Show-MainWindow { $control = $controlsList[$i] $matchFound = $false $controlToHighlight = $null - + if ($control -is [System.Windows.Controls.CheckBox]) { if ($control.Content.ToString().ToLower().Contains($searchText)) { $matchFound = $true @@ -1417,14 +530,13 @@ function Show-MainWindow { elseif ($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder') { $labelText = if ($control.Child) { $control.Child.Text.ToLower() } else { "" } $comboBox = if ($i + 1 -lt $controlsList.Count -and $controlsList[$i + 1] -is [System.Windows.Controls.ComboBox]) { $controlsList[$i + 1] } else { $null } - - # Check label text or combo box items - if ($labelText.Contains($searchText) -or ($comboBox -and (ComboBoxContainsMatch -comboBox $comboBox -searchText $searchText))) { + + if ($labelText.Contains($searchText) -or ($comboBox -and (Test-ComboBoxContainsMatch -ComboBox $comboBox -SearchText $searchText))) { $matchFound = $true $controlToHighlight = $control } } - + if ($matchFound -and $controlToHighlight) { $controlToHighlight.Background = $highlightBrush if ($null -eq $firstMatch) { $firstMatch = $controlToHighlight } @@ -1433,29 +545,34 @@ function Show-MainWindow { } } } - - # Scroll to first match if not visible + if ($firstMatch -and $tweaksScrollViewer) { - ScrollToItemIfNotVisible -scrollViewer $tweaksScrollViewer -item $firstMatch -container $tweaksGrid + Scroll-ToItemIfNotVisible -ScrollViewer $tweaksScrollViewer -Item $firstMatch -Container $tweaksGrid } }) - # Add Ctrl+F keyboard shortcut to focus search box on current tab + # ---- Show currently applied tweaks checkbox ---- + if ($ShowCurrentlyAppliedTweaksCheckBox) { + $ShowCurrentlyAppliedTweaksCheckBox.Add_Checked({ + Reset-TweaksToSystemState -Window $window -LoadSystemState $true + Update-AppliedTweaksUserModeState -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox -UserSelectionCombo $userSelectionCombo + }) + $ShowCurrentlyAppliedTweaksCheckBox.Add_Unchecked({ + Reset-TweaksToSystemState -Window $window -LoadSystemState $false + Update-AppliedTweaksUserModeState -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox -UserSelectionCombo $userSelectionCombo + }) + } + + # ---- Ctrl+F keyboard shortcut ---- $window.Add_KeyDown({ param($sourceControl, $e) - - # Check if Ctrl+F was pressed - if ($e.Key -eq [System.Windows.Input.Key]::F -and + if ($e.Key -eq [System.Windows.Input.Key]::F -and ([System.Windows.Input.Keyboard]::Modifiers -band [System.Windows.Input.ModifierKeys]::Control)) { - $currentTab = $tabControl.SelectedItem - - # Focus AppSearchBox if on App Removal tab if ($currentTab.Header -eq "App Removal" -and $appSearchBox) { $appSearchBox.Focus() $e.Handled = $true } - # Focus TweakSearchBox if on Tweaks tab elseif ($currentTab.Header -eq "Tweaks" -and $tweakSearchBox) { $tweakSearchBox.Focus() $e.Handled = $true @@ -1463,294 +580,87 @@ function Show-MainWindow { } }) - # Wizard Navigation - $tabControl = $window.FindName('MainTabControl') - $previousBtn = $window.FindName('PreviousBtn') - $nextBtn = $window.FindName('NextBtn') - $userSelectionCombo = $window.FindName('UserSelectionCombo') - $userSelectionDescription = $window.FindName('UserSelectionDescription') - $otherUserPanel = $window.FindName('OtherUserPanel') - $otherUsernameTextBox = $window.FindName('OtherUsernameTextBox') - $usernameTextBoxPlaceholder = $window.FindName('UsernameTextBoxPlaceholder') - $usernameValidationMessage = $window.FindName('UsernameValidationMessage') - $appRemovalScopeCombo = $window.FindName('AppRemovalScopeCombo') - $appRemovalScopeDescription = $window.FindName('AppRemovalScopeDescription') - $appRemovalScopeSection = $window.FindName('AppRemovalScopeSection') - $appRemovalScopeCurrentUser = $window.FindName('AppRemovalScopeCurrentUser') - $appRemovalScopeTargetUser = $window.FindName('AppRemovalScopeTargetUser') - - # Navigation button handlers - function UpdateNavigationButtons { - $currentIndex = $tabControl.SelectedIndex - $totalTabs = $tabControl.Items.Count - - $homeIndex = 0 - $overviewIndex = $totalTabs - 1 - - # Navigation button visibility - if ($currentIndex -eq $homeIndex) { - $nextBtn.Visibility = 'Collapsed' - $previousBtn.Visibility = 'Collapsed' - } elseif ($currentIndex -eq $overviewIndex) { - $nextBtn.Visibility = 'Collapsed' - $previousBtn.Visibility = 'Visible' - } else { - $nextBtn.Visibility = 'Visible' - $previousBtn.Visibility = 'Visible' - } - - # Update progress indicators - # Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Deployment Settings - $progressIndicator1 = $window.FindName('ProgressIndicator1') # App Removal - $progressIndicator2 = $window.FindName('ProgressIndicator2') # Tweaks - $progressIndicator3 = $window.FindName('ProgressIndicator3') # Deployment Settings - $bottomNavGrid = $window.FindName('BottomNavGrid') - - # Hide bottom navigation on home page - if ($currentIndex -eq 0) { - $bottomNavGrid.Visibility = 'Collapsed' - } else { - $bottomNavGrid.Visibility = 'Visible' - } - - # Update indicator colors based on current tab - # Indicator 1 (App Removal) - tab index 1 - if ($currentIndex -ge 1) { - $progressIndicator1.Fill = $window.Resources['ProgressActiveColor'] - } else { - $progressIndicator1.Fill = $window.Resources['ProgressInactiveColor'] - } - - # Indicator 2 (Tweaks) - tab index 2 - if ($currentIndex -ge 2) { - $progressIndicator2.Fill = $window.Resources['ProgressActiveColor'] - } else { - $progressIndicator2.Fill = $window.Resources['ProgressInactiveColor'] - } - - # Indicator 3 (Deployment Settings) - tab index 3 - if ($currentIndex -ge 3) { - $progressIndicator3.Fill = $window.Resources['ProgressActiveColor'] - } else { - $progressIndicator3.Fill = $window.Resources['ProgressInactiveColor'] - } - } - - # Update user selection description and show/hide other user panel - $userSelectionCombo.Add_SelectionChanged({ - switch ($userSelectionCombo.SelectedIndex) { - 0 { - $userSelectionDescription.Text = "Changes will be applied to the currently logged-in user profile." - $otherUserPanel.Visibility = 'Collapsed' - $usernameValidationMessage.Text = "" - # Show "Current user only" option, hide "Target user only" option - $appRemovalScopeCurrentUser.Visibility = 'Visible' - $appRemovalScopeTargetUser.Visibility = 'Collapsed' - $appRemovalScopeCombo.SelectedIndex = 0 - } - 1 { - $userSelectionDescription.Text = "Changes will be applied to a different user profile on this system." - $otherUserPanel.Visibility = 'Visible' - $usernameValidationMessage.Text = "" - # Hide "Current user only" option, show "Target user only" option - $appRemovalScopeCurrentUser.Visibility = 'Collapsed' - $appRemovalScopeTargetUser.Visibility = 'Visible' - $appRemovalScopeCombo.SelectedIndex = 0 - } - 2 { - $userSelectionDescription.Text = "Changes will be applied to the default user template, affecting all new users created after this point. Useful for Sysprep deployment." - $otherUserPanel.Visibility = 'Collapsed' - $usernameValidationMessage.Text = "" - # Hide other user options since they don't apply to default user template - $appRemovalScopeCurrentUser.Visibility = 'Collapsed' - $appRemovalScopeTargetUser.Visibility = 'Collapsed' - # Lock app removal scope to "All users" when applying to sysprep - $appRemovalScopeCombo.SelectedIndex = 0 - } - } - - # Keep enabled/disabled state in sync with both app selection and user mode. - UpdateAppSelectionStatus - }) - - # Helper function to update app removal scope description - function UpdateAppRemovalScopeDescription { - $selectedItem = $appRemovalScopeCombo.SelectedItem - if ($selectedItem) { - switch ($selectedItem.Content) { - "All users" { - $appRemovalScopeDescription.Text = "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users." - } - "Current user only" { - $appRemovalScopeDescription.Text = "Apps will only be removed for the current user. Other users and new users will not be affected." - } - "Target user only" { - $appRemovalScopeDescription.Text = "Apps will only be removed for the specified target user. Other users and new users will not be affected." - } - } - } - } - - # Update app removal scope description - $appRemovalScopeCombo.Add_SelectionChanged({ - UpdateAppRemovalScopeDescription - }) - - $otherUsernameTextBox.Add_TextChanged({ - # Show/hide placeholder - if ([string]::IsNullOrWhiteSpace($otherUsernameTextBox.Text)) { - $usernameTextBoxPlaceholder.Visibility = 'Visible' - } else { - $usernameTextBoxPlaceholder.Visibility = 'Collapsed' - } - - ValidateOtherUsername - }) - - function ValidateOtherUsername { - # Only validate if "Other User" is selected - if ($userSelectionCombo.SelectedIndex -ne 1) { - return $true - } - - $errorBrush = $window.Resources['ValidationErrorColor'] - $successBrush = $window.Resources['ValidationSuccessColor'] - $validationResult = Test-TargetUserName -UserName $otherUsernameTextBox.Text - - $usernameValidationMessage.Text = $validationResult.Message - if ($validationResult.IsValid) { - $usernameValidationMessage.Foreground = $successBrush - return $true - } - - $usernameValidationMessage.Foreground = $errorBrush - return $false - } - - function GenerateOverview { - $changesList = @() - - # Collect selected apps - $selectedAppsCount = 0 - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { - $selectedAppsCount++ - } - } - if ($selectedAppsCount -gt 0) { - $changesList += "Remove $selectedAppsCount application(s)" - } - - UpdateAppSelectionStatus - - # Collect all ComboBox/CheckBox selections from dynamically created controls - if ($script:UiControlMappings) { - foreach ($mappingKey in $script:UiControlMappings.Keys) { - $control = $window.FindName($mappingKey) - $isSelected = $false - - # Check if it's a checkbox or combobox - if ($control -is [System.Windows.Controls.CheckBox]) { - $isSelected = $control.IsChecked -eq $true - } - elseif ($control -is [System.Windows.Controls.ComboBox]) { - $isSelected = $control.SelectedIndex -gt 0 - } - - if ($control -and $isSelected) { - $mapping = $script:UiControlMappings[$mappingKey] - if ($mapping.Type -eq 'group') { - # For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values - $selectedValue = $mapping.Values[$control.SelectedIndex - 1] - foreach ($fid in $selectedValue.FeatureIds) { - $label = $script:FeatureLabelLookup[$fid] - if ($label) { $changesList += $label } - } - } - elseif ($mapping.Type -eq 'feature') { - $label = $script:FeatureLabelLookup[$mapping.FeatureId] - if (-not $label) { $label = $mapping.Label } - $changesList += $label - } - } - } - } - - return $changesList - } - - function ShowChangesOverview { - $changesList = GenerateOverview - - if ($changesList.Count -eq 0) { - Show-MessageBox -Message 'No changes have been selected.' -Title 'Selected Changes' -Button 'OK' -Icon 'Information' - return - } - - $message = ($changesList | ForEach-Object { "$([char]0x2022) $_" }) -join "`n" - Show-MessageBox -Message $message -Title 'Selected Changes' -Button 'OK' -Icon 'None' -Width 600 + # ---- Navigation button handlers ---- + function Invoke-NavigationUpdate { + Update-NavigationButtons -Window $window -TabControl $tabControl } $previousBtn.Add_Click({ Hide-Bubble -Immediate if ($tabControl.SelectedIndex -gt 0) { $tabControl.SelectedIndex-- - UpdateNavigationButtons + Invoke-NavigationUpdate } }) $nextBtn.Add_Click({ if ($tabControl.SelectedIndex -lt ($tabControl.Items.Count - 1)) { $tabControl.SelectedIndex++ - UpdateNavigationButtons + Invoke-NavigationUpdate } }) - # Handle Home Start button - $homeStartBtn = $window.FindName('HomeStartBtn') - $homeStartBtn.Add_Click({ - # Navigate to first tab after home (App Removal) - $tabControl.SelectedIndex = 1 - UpdateNavigationButtons - }) + # ---- User selection combo ---- + $userSelectionCombo.Add_SelectionChanged({ + Update-UserSelectionDescription -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UserSelectionDescription $userSelectionDescription - # Handle Home Default Mode button - apply defaults and navigate directly to overview - $homeDefaultModeBtn = $window.FindName('HomeDefaultModeBtn') - $homeDefaultModeBtn.Add_Click({ - # Load and apply default settings - $defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" - if ($defaultsJson) { - ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings + switch ($userSelectionCombo.SelectedIndex) { + 0 { + $otherUserPanel.Visibility = 'Collapsed' + $usernameValidationMessage.Text = "" + $appRemovalScopeCurrentUser.Visibility = 'Visible' + $appRemovalScopeTargetUser.Visibility = 'Collapsed' + $appRemovalScopeCombo.SelectedIndex = 0 + if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -ne $true) { + $ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $true + } + } + 1 { + $otherUserPanel.Visibility = 'Visible' + $usernameValidationMessage.Text = "" + $appRemovalScopeCurrentUser.Visibility = 'Collapsed' + $appRemovalScopeTargetUser.Visibility = 'Visible' + $appRemovalScopeCombo.SelectedIndex = 0 + if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) { + $ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false + } + } + 2 { + $otherUserPanel.Visibility = 'Collapsed' + $usernameValidationMessage.Text = "" + $appRemovalScopeCurrentUser.Visibility = 'Collapsed' + $appRemovalScopeTargetUser.Visibility = 'Collapsed' + $appRemovalScopeCombo.SelectedIndex = 0 + if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) { + $ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false + } + } } - # Deselect all apps, then select default apps (defer if apps are still loading in the background) - if ($script:IsLoadingApps) { - $script:PendingDefaultMode = $true - } else { - ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive + Update-AppSelectionStatus -AppsPanel $appsPanel -AppSelectionStatus $appSelectionStatus -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection -AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo + Update-AppliedTweaksUserModeState -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox -UserSelectionCombo $userSelectionCombo + }) + + # ---- App removal scope combo ---- + $appRemovalScopeCombo.Add_SelectionChanged({ + Update-AppRemovalScopeDescription -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeDescription $appRemovalScopeDescription + }) + + # ---- Other username text box ---- + $otherUsernameTextBox.Add_TextChanged({ + if ([string]::IsNullOrWhiteSpace($otherUsernameTextBox.Text)) { + $usernameTextBoxPlaceholder.Visibility = 'Visible' } - - # Navigate directly to the Deployment Settings tab - $tabControl.SelectedIndex = 3 - UpdateNavigationButtons - - # Show contextual hint bubble for the Review Changes link - $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{ - Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here' - }) | Out-Null + else { + $usernameTextBoxPlaceholder.Visibility = 'Collapsed' + } + Update-UserSelectionDescription -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UserSelectionDescription $userSelectionDescription + Test-OtherUsername -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UsernameValidationMessage $usernameValidationMessage | Out-Null }) - # Handle Review Changes link button - $reviewChangesBtn = $window.FindName('ReviewChangesBtn') - $reviewChangesBtn.Add_Click({ - Hide-Bubble - ShowChangesOverview - }) - - # Handle Apply Changes button - validates and immediately starts applying changes - $deploymentApplyBtn = $window.FindName('DeploymentApplyBtn') - $deploymentApplyBtn.Add_Click({ - if (-not (ValidateOtherUsername)) { + # ---- Validate target user helper ---- + $ensureValidTargetUserOrWarn = { + if (-not (Test-OtherUsername -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UsernameValidationMessage $usernameValidationMessage)) { $validationMessage = if (-not [string]::IsNullOrWhiteSpace($usernameValidationMessage.Text)) { $usernameValidationMessage.Text } @@ -1758,12 +668,62 @@ function Show-MainWindow { "Please enter a valid username." } Show-MessageBox -Message $validationMessage -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null - return + return $false } + return $true + } + + # ---- Home Start button ---- + $homeStartBtn.Add_Click({ + if (-not (& $ensureValidTargetUserOrWarn)) { return } + $tabControl.SelectedIndex = 1 + Invoke-NavigationUpdate + }) + + # ---- Home Default Mode button ---- + $homeDefaultModeBtn.Add_Click({ + if (-not (& $ensureValidTargetUserOrWarn)) { return } + + if ($ShowCurrentlyAppliedTweaksCheckBox) { + $ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false + } + + $defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" + if ($defaultsJson) { + ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings + } + + if ($script:IsLoadingApps) { + $script:PendingDefaultMode = $true + } + else { + Invoke-AppPreset -AppsPanel $appsPanel -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive + } + + $tabControl.SelectedIndex = 3 + Invoke-NavigationUpdate + + $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{ + Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here' + }) | Out-Null + }) + + # ---- Review Changes link ---- + $reviewChangesBtn.Add_Click({ + Hide-Bubble + Invoke-ShowChangesOverview -Window $window -AppsPanel $appsPanel -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox + }) + + # ---- Apply Changes button ---- + $deploymentApplyBtn.Add_Click({ + if (-not (& $ensureValidTargetUserOrWarn)) { return } Hide-Bubble -Immediate - # App Removal - collect selected apps from integrated UI + $showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) + $selectedForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) + + # App Removal - collect selected apps $selectedApps = @() foreach ($child in $appsPanel.Children) { if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { @@ -1771,102 +731,49 @@ function Show-MainWindow { } } $selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique) - + $hasAppSelection = ($selectedApps.Count -gt 0) + if ($selectedApps.Count -gt 0) { - if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { - return - } - - + if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return } + AddParameter 'RemoveApps' AddParameter 'Apps' ($selectedApps -join ',') - - # Add app removal target parameter based on selection + $selectedScopeItem = $appRemovalScopeCombo.SelectedItem if ($selectedScopeItem) { switch ($selectedScopeItem.Content) { - "All users" { - AddParameter 'AppRemovalTarget' 'AllUsers' - } - "Current user only" { - AddParameter 'AppRemovalTarget' 'CurrentUser' - } - "Target user only" { - # Use the target username from Other User panel - AddParameter 'AppRemovalTarget' ($otherUsernameTextBox.Text.Trim()) - } + "All users" { AddParameter 'AppRemovalTarget' 'AllUsers' } + "Current user only" { AddParameter 'AppRemovalTarget' 'CurrentUser' } + "Target user only" { AddParameter 'AppRemovalTarget' ($otherUsernameTextBox.Text.Trim()) } } } } - # Apply dynamic tweaks selections - if ($script:UiControlMappings) { - foreach ($mappingKey in $script:UiControlMappings.Keys) { - $control = $window.FindName($mappingKey) - $isSelected = $false - $selectedIndex = 0 - - # Check if it's a checkbox or combobox - if ($control -is [System.Windows.Controls.CheckBox]) { - $isSelected = $control.IsChecked -eq $true - $selectedIndex = if ($isSelected) { 1 } else { 0 } - } - elseif ($control -is [System.Windows.Controls.ComboBox]) { - $isSelected = $control.SelectedIndex -gt 0 - $selectedIndex = $control.SelectedIndex - } - - if ($control -and $isSelected) { - $mapping = $script:UiControlMappings[$mappingKey] - if ($mapping.Type -eq 'group') { - if ($selectedIndex -gt 0 -and $selectedIndex -le $mapping.Values.Count) { - $selectedValue = $mapping.Values[$selectedIndex - 1] - foreach ($fid in $selectedValue.FeatureIds) { - AddParameter $fid - } - } - } - elseif ($mapping.Type -eq 'feature') { - AddParameter $mapping.FeatureId - } - } + # Apply dynamic tweaks + foreach ($tweakAction in @(Get-PendingTweakActions -Window $window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) { + if ($tweakAction.Action -eq 'Apply') { + AddParameter $tweakAction.FeatureId + $null = $selectedForwardFeatureIds.Add([string]$tweakAction.FeatureId) + continue } + $script:UndoParams[[string]$tweakAction.FeatureId] = $true } - $controlParamsCount = 0 - foreach ($Param in $script:ControlParams) { - if ($script:Params.ContainsKey($Param)) { - $controlParamsCount++ - } - } - - # Check if any changes were selected - $totalChanges = $script:Params.Count - $controlParamsCount - - # Apps parameter does not count as a change itself - if ($script:Params.ContainsKey('Apps')) { - $totalChanges = $totalChanges - 1 - } - - if ($totalChanges -eq 0) { + if (-not $hasAppSelection -and $selectedForwardFeatureIds.Count -eq 0 -and $script:UndoParams.Count -eq 0) { Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information' return } - # Check RestorePointCheckBox $restorePointCheckBox = $window.FindName('RestorePointCheckBox') if ($restorePointCheckBox -and $restorePointCheckBox.IsChecked) { AddParameter 'CreateRestorePoint' } - - # Store selected user mode + switch ($userSelectionCombo.SelectedIndex) { - 0 { - Write-Host "Selected user mode: current user ($(GetUserName))" - } - 1 { + 0 { Write-Host "Selected user mode: current user ($(GetUserName))" } + 1 { Write-Host "Selected user mode: $($otherUsernameTextBox.Text.Trim())" - AddParameter User ($otherUsernameTextBox.Text.Trim()) + AddParameter User ($otherUsernameTextBox.Text.Trim()) } 2 { Write-Host "Selected user mode: default user profile (Sysprep)" @@ -1876,352 +783,154 @@ function Show-MainWindow { SaveSettings - # Check if user wants to restart explorer $restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox') $shouldRestartExplorer = $restartExplorerCheckBox -and $restartExplorerCheckBox.IsChecked - # Show the apply changes window Show-ApplyModal -Owner $window -RestartExplorer $shouldRestartExplorer - - # Close the main window after the apply dialog closes $window.Close() }) - # Initialize UI elements on window load + # ---- Tweaks presets tri-state ---- + foreach ($presetCheckBox in @($presetDefaultTweaksBtn, $presetLastUsedTweaksBtn, $presetPrivacyTweaksBtn, $presetAITweaksBtn)) { + Add-TriStateClickBehavior -CheckBox $presetCheckBox + } + + # ---- Clear All Tweaks ---- + $clearAllTweaksBtn.Add_Click({ + if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) { + $ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false + } + Clear-TweakSelections -Window $window + Update-TweakPresetStates -Window $window + }) + + # ---- Window Load event ---- $window.Add_Loaded({ - BuildDynamicTweaks - UpdateTweaksResponsiveColumns - RefreshTweakPresetSources -defaultSettingsJson $defaultsJson -lastUsedSettingsJson $lastUsedSettingsJson - RegisterTweakPresetControlStateHandlers - UpdateTweakPresetStates + try { + & $updateHomeContentPosition + Build-DynamicTweaks -Window $window -WinVersion $WinVersion + Load-CurrentTweakStateIntoUI -Window $window + Update-TweaksResponsiveColumns -Window $window - LoadAppsIntoMainUI + $lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile + $defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" - # Update Current User label with username - if ($userSelectionCombo -and $userSelectionCombo.Items.Count -gt 0) { - $currentUserItem = $userSelectionCombo.Items[0] - if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) { - $currentUserItem.Content = "Current User ($(GetUserName))" + $script:SavedAppIds = Get-SavedAppIdsFromSettingsJson -SettingsJson $lastUsedSettingsJson + + Initialize-TweakPresetSources -Window $window -DefaultSettingsJson $defaultsJson -LastUsedSettingsJson $lastUsedSettingsJson + Register-TweakPresetControlStateHandlers -Window $window + Update-TweakPresetStates -Window $window + + Load-AppsIntoMainUI -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn + + # Update Current User label + if ($userSelectionCombo -and $userSelectionCombo.Items.Count -gt 0) { + $currentUserItem = $userSelectionCombo.Items[0] + if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) { + $currentUserItem.Content = "Current User ($(GetUserName))" + } } - } - # When running as SYSTEM, the "Current User" option is not meaningful. - # Hide it from the dropdown and default to "Other User". - $isSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18') - if ($isSystem -and $userSelectionCombo.Items.Count -gt 0) { - $currentUserItem = $userSelectionCombo.Items[0] - if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) { - $currentUserItem.Visibility = 'Collapsed' - $currentUserItem.IsEnabled = $false + # When running as SYSTEM, the "Current User" option is not meaningful. + # Hide it from the dropdown and default to "Other User". + $isSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18') + if ($isSystem -and $userSelectionCombo.Items.Count -gt 0) { + $currentUserItem = $userSelectionCombo.Items[0] + if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) { + $currentUserItem.Visibility = 'Collapsed' + $currentUserItem.IsEnabled = $false + } + $userSelectionCombo.SelectedIndex = 1 } - $userSelectionCombo.SelectedIndex = 1 - } - # Disable Restart Explorer option if NoRestartExplorer parameter is set - $restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox') - if ($restartExplorerCheckBox -and $script:Params.ContainsKey("NoRestartExplorer")) { - $restartExplorerCheckBox.IsChecked = $false - $restartExplorerCheckBox.IsEnabled = $false - } + $restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox') + if ($restartExplorerCheckBox -and $script:Params.ContainsKey("NoRestartExplorer")) { + $restartExplorerCheckBox.IsChecked = $false + $restartExplorerCheckBox.IsEnabled = $false + } - # Force Apply Changes To setting if Sysprep or User parameters are set - if ($script:Params.ContainsKey("Sysprep")) { - $userSelectionCombo.SelectedIndex = 2 - $userSelectionCombo.IsEnabled = $false - } - elseif ($script:Params.ContainsKey("User")) { - $userSelectionCombo.SelectedIndex = 1 - $userSelectionCombo.IsEnabled = $false - $otherUsernameTextBox.Text = $script:Params.Item("User") - $otherUsernameTextBox.IsEnabled = $false - } + if ($script:Params.ContainsKey("Sysprep")) { + $userSelectionCombo.SelectedIndex = 2 + $userSelectionCombo.IsEnabled = $false + } + elseif ($script:Params.ContainsKey("User")) { + $userSelectionCombo.SelectedIndex = 1 + $userSelectionCombo.IsEnabled = $false + $otherUsernameTextBox.Text = $script:Params.Item("User") + $otherUsernameTextBox.IsEnabled = $false + } - UpdateNavigationButtons + Update-UserSelectionDescription -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UserSelectionDescription $userSelectionDescription + Update-AppliedTweaksUserModeState -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox -UserSelectionCombo $userSelectionCombo + Invoke-NavigationUpdate + } + catch { + Write-Warning "Error during GUI initialization: $($_.Exception.Message)" + Write-Warning "Stack trace: $($_.Exception.StackTrace)" + Show-MessageBox -Message "An error occurred during initialization: $($_.Exception.Message)" -Title "Initialization Error" -Button 'OK' -Icon 'Error' | Out-Null + } }) - # Add event handler for tab changes + # ---- Tab change event ---- $tabControl.Add_SelectionChanged({ - # Regenerate overview when switching to Overview tab if ($tabControl.SelectedIndex -eq ($tabControl.Items.Count - 2)) { - GenerateOverview + New-Overview -Window $window -AppsPanel $appsPanel -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox | Out-Null } - UpdateNavigationButtons + Invoke-NavigationUpdate }) - function BuildTweakPresetControlMap { - param($settingsJson) - - $presetMap = @{} - if (-not $settingsJson -or -not $settingsJson.Settings -or -not $script:UiControlMappings) { - return $presetMap - } - - # FeatureId -> control metadata, similar to ApplySettingsToUiControls lookup. - $featureIdIndex = @{} - foreach ($controlName in $script:UiControlMappings.Keys) { - $control = $window.FindName($controlName) - if (-not $control -or $control.Visibility -ne 'Visible') { continue } - - $mapping = $script:UiControlMappings[$controlName] - if ($mapping.Type -eq 'group') { - $i = 1 - foreach ($val in $mapping.Values) { - foreach ($fid in $val.FeatureIds) { - $featureIdIndex[$fid] = @{ ControlName = $controlName; Control = $control; MappingType = 'group'; Index = $i } - } - $i++ - } - } - elseif ($mapping.Type -eq 'feature') { - $featureIdIndex[$mapping.FeatureId] = @{ ControlName = $controlName; Control = $control; MappingType = 'feature' } - } - } - - foreach ($setting in $settingsJson.Settings) { - if ($setting.Value -ne $true) { continue } - if ($setting.Name -eq 'CreateRestorePoint') { continue } - - $entry = $featureIdIndex[$setting.Name] - if (-not $entry) { continue } - if ($presetMap.ContainsKey($entry.ControlName)) { continue } - - $controlType = if ($entry.Control -is [System.Windows.Controls.CheckBox]) { 'CheckBox' } else { 'ComboBox' } - $desiredValue = switch ($entry.MappingType) { - 'group' { $entry.Index } - default { if ($controlType -eq 'CheckBox') { $true } else { 1 } } - } - - $presetMap[$entry.ControlName] = @{ Control = $entry.Control; ControlType = $controlType; DesiredValue = $desiredValue } - } - - return $presetMap - } - - function BuildCategoryTweakPresetMap { - param([string]$Category) - - $presetMap = @{} - if (-not $script:UiControlMappings) { return $presetMap } - - foreach ($controlName in $script:UiControlMappings.Keys) { - $mapping = $script:UiControlMappings[$controlName] - if ($mapping.Category -ne $Category) { continue } - - $control = $window.FindName($controlName) - if (-not $control -or $control.Visibility -ne 'Visible') { continue } - - $controlType = if ($control -is [System.Windows.Controls.CheckBox]) { 'CheckBox' } else { 'ComboBox' } - $desiredValue = if ($controlType -eq 'CheckBox') { $true } else { 1 } - $presetMap[$controlName] = @{ Control = $control; ControlType = $controlType; DesiredValue = $desiredValue } - } - - return $presetMap - } - - function GetSavedAppIdsFromSettingsJson { - param($settingsJson) - - if (-not $settingsJson -or -not $settingsJson.Settings) { - return $null - } - - $appsValue = $null - foreach ($setting in $settingsJson.Settings) { - if ($setting.Name -eq 'Apps' -and $setting.Value) { - $appsValue = $setting.Value - break - } - } - - if (-not $appsValue) { - return $null - } - - $savedAppIds = @() - if ($appsValue -is [string]) { - $savedAppIds = $appsValue.Split(',') - } - elseif ($appsValue -is [array]) { - $savedAppIds = $appsValue - } - - $savedAppIds = $savedAppIds | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } - if ($savedAppIds.Count -eq 0) { - return $null - } - - return $savedAppIds - } - - function ApplyTweakPresetMap { - param( - [hashtable]$PresetMap, - [bool]$Check - ) - - if (-not $PresetMap) { - $PresetMap = @{} - } - - $wasUpdatingTweakPresets = [bool]$script:UpdatingTweakPresets - $script:UpdatingTweakPresets = $true - try { - foreach ($target in $PresetMap.Values) { - $control = $target.Control - if (-not $control) { continue } - - if ($target.ControlType -eq 'CheckBox') { - $control.IsChecked = $Check - } - elseif ($target.ControlType -eq 'ComboBox') { - $desiredIndex = [int]$target.DesiredValue - if ($Check) { - $control.SelectedIndex = $desiredIndex - } - elseif ($control.SelectedIndex -eq $desiredIndex) { - $control.SelectedIndex = 0 - } - } - } - } - finally { - $script:UpdatingTweakPresets = $wasUpdatingTweakPresets - } - - if (-not $wasUpdatingTweakPresets) { - UpdateTweakPresetStates - } - } - - function SetTweakPresetState { - param( - [System.Windows.Controls.CheckBox]$PresetCheckBox, - [hashtable]$PresetMap - ) - - if (-not $PresetCheckBox) { return } - if (-not $PresetMap) { - $PresetMap = @{} - } - - $total = $PresetMap.Count - $selected = 0 - - foreach ($target in $PresetMap.Values) { - $control = $target.Control - if (-not $control) { continue } - - if ($target.ControlType -eq 'CheckBox' -and $control.IsChecked -eq $true) { - $selected++ - } - elseif ($target.ControlType -eq 'ComboBox' -and $control.SelectedIndex -eq [int]$target.DesiredValue) { - $selected++ - } - } - - SetTriStatePresetCheckBoxState -CheckBox $PresetCheckBox -Total $total -Selected $selected - } - - function UpdateTweakPresetStates { - $script:UpdatingTweakPresets = $true - try { - SetTweakPresetState -PresetCheckBox $presetDefaultTweaksBtn -PresetMap $script:DefaultTweakPresetMap - if ($presetLastUsedTweaksBtn -and $presetLastUsedTweaksBtn.Visibility -ne 'Collapsed') { - SetTweakPresetState -PresetCheckBox $presetLastUsedTweaksBtn -PresetMap $script:LastUsedTweakPresetMap - } - SetTweakPresetState -PresetCheckBox $presetPrivacyTweaksBtn -PresetMap $script:PrivacyTweakPresetMap - SetTweakPresetState -PresetCheckBox $presetAITweaksBtn -PresetMap $script:AITweakPresetMap - } - finally { - $script:UpdatingTweakPresets = $false - } - } - - function RegisterTweakPresetControlStateHandlers { - if (-not $script:UiControlMappings) { return } - - foreach ($controlName in $script:UiControlMappings.Keys) { - $control = $window.FindName($controlName) - if (-not $control) { continue } - - if ($control -is [System.Windows.Controls.CheckBox]) { - $control.Add_Checked({ if (-not $script:UpdatingTweakPresets) { UpdateTweakPresetStates } }) - $control.Add_Unchecked({ if (-not $script:UpdatingTweakPresets) { UpdateTweakPresetStates } }) - } - elseif ($control -is [System.Windows.Controls.ComboBox]) { - $control.Add_SelectionChanged({ if (-not $script:UpdatingTweakPresets) { UpdateTweakPresetStates } }) - } - } - } - - function RefreshTweakPresetSources { - param( - $defaultSettingsJson, - $lastUsedSettingsJson - ) - - $script:DefaultTweakPresetMap = BuildTweakPresetControlMap -settingsJson $defaultSettingsJson - $script:LastUsedTweakPresetMap = BuildTweakPresetControlMap -settingsJson $lastUsedSettingsJson - $script:PrivacyTweakPresetMap = BuildCategoryTweakPresetMap -Category 'Privacy & Suggested Content' - $script:AITweakPresetMap = BuildCategoryTweakPresetMap -Category 'AI' - - if ($presetLastUsedTweaksBtn) { - $presetLastUsedTweaksBtn.Visibility = if ($script:LastUsedTweakPresetMap.Count -gt 0) { 'Visible' } else { 'Collapsed' } - } - } - + # ---- Tweak presets wiring ---- $lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile - $defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" $script:DefaultTweakPresetMap = @{} $script:LastUsedTweakPresetMap = @{} $script:PrivacyTweakPresetMap = @{} $script:AITweakPresetMap = @{} - $script:SavedAppIds = GetSavedAppIdsFromSettingsJson -settingsJson $lastUsedSettingsJson + $script:SavedAppIds = Get-SavedAppIdsFromSettingsJson -SettingsJson $lastUsedSettingsJson if ($presetDefaultTweaksBtn) { $presetDefaultTweaksBtn.Add_Click({ if ($script:UpdatingTweakPresets) { return } - $check = NormalizeCheckboxState -checkBox $this - ApplyTweakPresetMap -PresetMap $script:DefaultTweakPresetMap -Check $check + $check = ConvertTo-NormalizedCheckboxState -CheckBox $this + Invoke-ApplyTweakPresetMap -PresetMap $script:DefaultTweakPresetMap -Check $check }) } if ($presetLastUsedTweaksBtn) { $presetLastUsedTweaksBtn.Add_Click({ if ($script:UpdatingTweakPresets) { return } - $check = NormalizeCheckboxState -checkBox $this - ApplyTweakPresetMap -PresetMap $script:LastUsedTweakPresetMap -Check $check + $check = ConvertTo-NormalizedCheckboxState -CheckBox $this + Invoke-ApplyTweakPresetMap -PresetMap $script:LastUsedTweakPresetMap -Check $check }) } if ($presetPrivacyTweaksBtn) { $presetPrivacyTweaksBtn.Add_Click({ if ($script:UpdatingTweakPresets) { return } - $check = NormalizeCheckboxState -checkBox $this - ApplyTweakPresetMap -PresetMap $script:PrivacyTweakPresetMap -Check $check + $check = ConvertTo-NormalizedCheckboxState -CheckBox $this + Invoke-ApplyTweakPresetMap -PresetMap $script:PrivacyTweakPresetMap -Check $check }) } if ($presetAITweaksBtn) { $presetAITweaksBtn.Add_Click({ if ($script:UpdatingTweakPresets) { return } - $check = NormalizeCheckboxState -checkBox $this - ApplyTweakPresetMap -PresetMap $script:AITweakPresetMap -Check $check + $check = ConvertTo-NormalizedCheckboxState -CheckBox $this + Invoke-ApplyTweakPresetMap -PresetMap $script:AITweakPresetMap -Check $check }) } - # Hide Last used tweak preset by default; it is shown after dynamic controls are built and mappings are resolved. + # Hide Last used tweak preset by default if ($presetLastUsedTweaksBtn) { $presetLastUsedTweaksBtn.Visibility = 'Collapsed' } - # Preset: Last used selection (wired to PresetLastUsed checkbox) + # ---- Preset: Last used selection (apps) ---- if ($script:SavedAppIds) { $presetLastUsed.Add_Click({ if ($script:UpdatingPresets) { return } - $check = NormalizeCheckboxState -checkBox $this - ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } -Check $check + $check = ConvertTo-NormalizedCheckboxState -CheckBox $this + Invoke-AppPreset -AppsPanel $appsPanel -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } -Check $check }) } else { @@ -2229,14 +938,7 @@ function Show-MainWindow { $presetLastUsed.Visibility = 'Collapsed' } - # Clear All Tweaks button - $clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn') - $clearAllTweaksBtn.Add_Click({ - ClearTweakSelections - UpdateTweakPresetStates - }) - - # Preload app data to speed up loading when user navigates to App Removal tab + # ---- Preload app data ---- try { $script:PreloadedAppData = LoadAppsDetailsFromJson -OnlyInstalled:$false -InstalledList '' -InitialCheckedFromJson:$false } @@ -2244,7 +946,7 @@ function Show-MainWindow { Write-Warning "Failed to preload apps list: $_" } - # Show the window + # ---- Show window ---- $frame = [System.Windows.Threading.DispatcherFrame]::new() $window.Add_Closed({ $frame.Continue = $false diff --git a/Scripts/GUI/Show-RestoreBackupDialog.ps1 b/Scripts/GUI/Show-RestoreBackupDialog.ps1 index e53e1ab..8fea4db 100644 --- a/Scripts/GUI/Show-RestoreBackupDialog.ps1 +++ b/Scripts/GUI/Show-RestoreBackupDialog.ps1 @@ -70,6 +70,9 @@ function Show-RestoreBackupDialog { $backupCreatedText = $window.FindName('BackupCreatedText') $backupTargetText = $window.FindName('BackupTargetText') $featuresItemsControl = $window.FindName('FeaturesItemsControl') + $reappliedSeparator = $window.FindName('ReappliedSeparator') + $reappliedPanel = $window.FindName('ReappliedPanel') + $reappliedFeaturesItemsControl = $window.FindName('ReappliedFeaturesItemsControl') $nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator') $nonRevertiblePanel = $window.FindName('NonRevertiblePanel') $nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl') @@ -119,6 +122,8 @@ function Show-RestoreBackupDialog { $overviewFeaturesSection.Visibility = 'Collapsed' $overviewSummaryText.Visibility = 'Visible' + $reappliedSeparator.Visibility = 'Collapsed' + $reappliedPanel.Visibility = 'Collapsed' $nonRevertibleSeparator.Visibility = 'Collapsed' $nonRevertiblePanel.Visibility = 'Collapsed' $introInfoPanel.Visibility = 'Collapsed' @@ -215,13 +220,33 @@ function Show-RestoreBackupDialog { } } - $selectedFeatureIds = Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup - $featureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedFeatureIds -Features $script:Features - $revertibleFeaturesList = @($featureLists.Revertible) - $nonRevertibleFeaturesList = @($featureLists.NonRevertible) - Write-Host "Backup overview prepared. Revertible=$($revertibleFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)" + $selectedForwardFeatureIds = @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup) + $selectedUndoFeatureIds = @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup) - if ($revertibleFeaturesList.Count -eq 0) { + $seenForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) + foreach ($featureId in $selectedForwardFeatureIds) { + [void]$seenForwardFeatureIds.Add([string]$featureId) + } + + $filteredUndoFeatureIds = New-Object System.Collections.Generic.List[string] + foreach ($featureId in $selectedUndoFeatureIds) { + if ($seenForwardFeatureIds.Contains([string]$featureId)) { + continue + } + + $filteredUndoFeatureIds.Add([string]$featureId) + } + + $forwardFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedForwardFeatureIds -Features $script:Features + $undoFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds @($filteredUndoFeatureIds.ToArray()) -Features $script:Features + $combinedFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds (Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup) -Features $script:Features + + $revertibleFeaturesList = @($forwardFeatureLists.Revertible) + $reappliedFeaturesList = @($undoFeatureLists.Revertible) + $nonRevertibleFeaturesList = @($combinedFeatureLists.NonRevertible) + Write-Host "Backup overview prepared. Reverted=$($revertibleFeaturesList.Count), ReApplied=$($reappliedFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)" + + if ($revertibleFeaturesList.Count -eq 0 -and $reappliedFeaturesList.Count -eq 0) { throw 'The selected backup does not contain any changes that can be restored.' } @@ -229,13 +254,16 @@ function Show-RestoreBackupDialog { $backupCreatedText.Text = $createdText $backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target) $featuresItemsControl.ItemsSource = $revertibleFeaturesList - $overviewFeaturesSection.Visibility = 'Visible' + $overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' } + $reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList + if ($reappliedFeaturesList.Count -gt 0) { $reappliedPanel.Visibility = 'Visible' } else { $reappliedPanel.Visibility = 'Collapsed' } + if ($revertibleFeaturesList.Count -gt 0 -and $reappliedFeaturesList.Count -gt 0) { $reappliedSeparator.Visibility = 'Visible' } else { $reappliedSeparator.Visibility = 'Collapsed' } $overviewSummaryText.Visibility = 'Collapsed' $nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList $hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0) if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' } - if ($hasNonRevertibleItems) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' } + if ($hasNonRevertibleItems -and ($revertibleFeaturesList.Count -gt 0 -or $reappliedFeaturesList.Count -gt 0)) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' } $introInfoPanel.Visibility = 'Collapsed' $overviewPanel.Visibility = 'Visible' @@ -295,6 +323,30 @@ function Show-RestoreBackupDialog { return } + if (-not $useManualBackupFile) { + $autoBackupExists = $false + if ($scope -eq 'AllUsers') { + $userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" + $usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue + foreach ($startMenuPath in $usersStartMenuPaths) { + if (Test-Path -LiteralPath (Join-Path $startMenuPath.FullName 'start2.bin.bak')) { + $autoBackupExists = $true + break + } + } + } + else { + $autoBackupPath = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin.bak" + $autoBackupExists = Test-Path -LiteralPath $autoBackupPath + } + + if (-not $autoBackupExists) { + $scopeText = (& $getStartMenuScopeInfo).SummaryText + Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found. You can uncheck the 'Automatically find Start Menu backup' option to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null + return + } + } + $window.Tag = @{ Result = 'RestoreStartMenu' StartMenuScope = $scope diff --git a/Scripts/GUI/Show-RestoreBackupWindow.ps1 b/Scripts/GUI/Show-RestoreBackupWindow.ps1 index 12884b2..a5eaac2 100644 --- a/Scripts/GUI/Show-RestoreBackupWindow.ps1 +++ b/Scripts/GUI/Show-RestoreBackupWindow.ps1 @@ -7,10 +7,15 @@ function Show-RestoreBackupWindow { try { Write-Host 'Opening restore backup dialog.' + $restoreResult = [PSCustomObject]@{ + RestoredRegistry = $false + RestoredStartMenu = $false + } + $dialogResult = Show-RestoreBackupDialog -Owner $Owner if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') { Write-Host 'Restore canceled by user.' - return + return $restoreResult } $successMessage = $null @@ -24,7 +29,8 @@ function Show-RestoreBackupWindow { Write-Host "User confirmed registry restore for $($backup.Target)." Restore-RegistryBackupState -Backup $backup - $successMessage = 'Registry backup restored successfully. Please restart your computer for all changes to take effect.' + $restoreResult.RestoredRegistry = $true + $successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.' } elseif ($dialogResult.Result -eq 'RestoreStartMenu') { $scope = $dialogResult.StartMenuScope @@ -69,6 +75,8 @@ function Show-RestoreBackupWindow { $successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in." } } + + $restoreResult.RestoredStartMenu = $true } if ($warningMessage) { @@ -79,10 +87,16 @@ function Show-RestoreBackupWindow { Write-Host "$successMessage" Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success } + + return $restoreResult } catch { $errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' } Write-Error "Restore operation failed: $errorMessage" Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error + return [PSCustomObject]@{ + RestoredRegistry = $false + RestoredStartMenu = $false + } } } diff --git a/Scripts/Get-Dev.ps1 b/Scripts/Get-Dev.ps1 index 45f43e6..4605e29 100644 --- a/Scripts/Get-Dev.ps1 +++ b/Scripts/Get-Dev.ps1 @@ -17,9 +17,7 @@ param ( [switch]$RemoveApps, [switch]$RemoveAppsCustom, [switch]$RemoveGamingApps, - [switch]$RemoveCommApps, [switch]$RemoveHPApps, - [switch]$RemoveW11Outlook, [switch]$ForceRemoveEdge, [switch]$DisableDVR, [switch]$DisableGameBarIntegration, @@ -58,7 +56,7 @@ param ( [switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb, [switch]$HideTaskview, [switch]$DisableStartRecommended, - [switch]$DisableStartAllApps, + [switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList, [switch]$DisableStartPhoneLink, [switch]$DisableCopilot, [switch]$DisableRecall, @@ -135,12 +133,12 @@ catch { Exit } -Write-Output "" -Write-Output "> Cleaning up old Win11Debloat folder..." - -# Remove old script folder if it exists, but keep config and log files +# Remove old script folder if it exists, but keep configs, logs and backups if (Test-Path $tempWorkPath) { - Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force + Write-Output "" + Write-Output "> Cleaning up old Win11Debloat folder..." + + Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force } $configDir = Join-Path $tempWorkPath 'Config' @@ -148,6 +146,9 @@ $backupDir = Join-Path $tempWorkPath 'ConfigOld' # Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked if (Test-Path "$configDir") { + Write-Output "" + Write-Output "> Backing up existing config files..." + New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null $filesToKeep = @( @@ -178,6 +179,9 @@ if (Test-Path "$backupDir") { New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null } + Write-Output "" + Write-Output "> Restoring existing config files..." + Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir" Remove-Item "$backupDir" -Recurse -Force } @@ -218,13 +222,13 @@ if ($null -ne $debloatProcess) { $debloatProcess.WaitForExit() } -# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files +# Remove all remaining script files, except for configs, logs and backups if (Test-Path $tempWorkPath) { Write-Output "" Write-Output "> Cleaning up..." # Cleanup, remove Win11Debloat directory - Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs,Backups | Remove-Item -Recurse -Force + Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force } Write-Output "" diff --git a/Scripts/Get.ps1 b/Scripts/Get.ps1 index 88d2f72..9ded2a1 100644 --- a/Scripts/Get.ps1 +++ b/Scripts/Get.ps1 @@ -17,9 +17,7 @@ param ( [switch]$RemoveApps, [switch]$RemoveAppsCustom, [switch]$RemoveGamingApps, - [switch]$RemoveCommApps, [switch]$RemoveHPApps, - [switch]$RemoveW11Outlook, [switch]$ForceRemoveEdge, [switch]$DisableDVR, [switch]$DisableGameBarIntegration, @@ -58,7 +56,7 @@ param ( [switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb, [switch]$HideTaskview, [switch]$DisableStartRecommended, - [switch]$DisableStartAllApps, + [switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList, [switch]$DisableStartPhoneLink, [switch]$DisableCopilot, [switch]$DisableRecall, @@ -174,7 +172,7 @@ Expand-Archive $tempArchivePath $tempWorkPath Remove-Item $tempArchivePath # Move files -Get-ChildItem -Path (Join-Path $tempWorkPath 'Raphire-Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath +Get-ChildItem -Path (Join-Path $tempWorkPath '*Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath # Add existing config files back to Config folder if (Test-Path "$backupDir") { diff --git a/Scripts/Helpers/Get-RegFileOperations.ps1 b/Scripts/Helpers/Get-RegFileOperations.ps1 index e3dea03..3cd22c2 100644 --- a/Scripts/Helpers/Get-RegFileOperations.ps1 +++ b/Scripts/Helpers/Get-RegFileOperations.ps1 @@ -129,10 +129,13 @@ function Convert-RegValueData { } if ($valueData -match '^"(?.*)"$') { + $stringValue = $matches.value + # Unescape registry string escape sequences + $stringValue = $stringValue -replace '\\"', '"' -replace '\\\\', '\' return [PSCustomObject]@{ OperationType = 'SetValue' ValueType = 'String' - ValueData = $matches.value + ValueData = $stringValue } } diff --git a/Scripts/Helpers/RegistryPathHelpers.ps1 b/Scripts/Helpers/RegistryPathHelpers.ps1 index 8615739..683fd49 100644 --- a/Scripts/Helpers/RegistryPathHelpers.ps1 +++ b/Scripts/Helpers/RegistryPathHelpers.ps1 @@ -70,14 +70,14 @@ function Get-RegistryRootKey { function Get-RegistryFilePathForFeature { param( [Parameter(Mandatory)] - $Feature, + [string]$RegistryKey, [switch]$UseSysprepRegFiles ) $useSysprepLayout = $UseSysprepRegFiles -or $script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User') if ($useSysprepLayout) { - return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $Feature.RegistryKey + return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $RegistryKey } - return Join-Path $script:RegfilesPath $Feature.RegistryKey + return Join-Path $script:RegfilesPath $RegistryKey } diff --git a/Win11Debloat.ps1 b/Win11Debloat.ps1 index f97dd18..fe205f0 100644 --- a/Win11Debloat.ps1 +++ b/Win11Debloat.ps1 @@ -17,9 +17,7 @@ param ( [switch]$RemoveApps, [switch]$RemoveAppsCustom, [switch]$RemoveGamingApps, - [switch]$RemoveCommApps, [switch]$RemoveHPApps, - [switch]$RemoveW11Outlook, [switch]$ForceRemoveEdge, [switch]$DisableDVR, [switch]$DisableGameBarIntegration, @@ -59,7 +57,7 @@ param ( [switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb, [switch]$HideTaskview, [switch]$DisableStartRecommended, - [switch]$DisableStartAllApps, + [switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList, [switch]$DisableStartPhoneLink, [switch]$DisableCopilot, [switch]$DisableRecall, @@ -293,6 +291,7 @@ if (-not $script:WingetInstalled -and -not $Silent) { . "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1" # Features functions +. "$PSScriptRoot/Scripts/Features/GetCurrentTweakState.ps1" . "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1" . "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1" . "$PSScriptRoot/Scripts/Features/BackupRegistryFeatureSelection.ps1" @@ -302,7 +301,7 @@ if (-not $script:WingetInstalled -and -not $Silent) { . "$PSScriptRoot/Scripts/Features/RestoreRegistryApplyState.ps1" . "$PSScriptRoot/Scripts/Features/RestoreRegistryBackup.ps1" . "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1" -. "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1" +. "$PSScriptRoot/Scripts/Features/WindowsOptionalFeatures.ps1" . "$PSScriptRoot/Scripts/Features/ImportRegistryFile.ps1" . "$PSScriptRoot/Scripts/Features/ReplaceStartMenu.ps1" . "$PSScriptRoot/Scripts/Features/RestartExplorer.ps1" @@ -330,6 +329,11 @@ if (-not $script:WingetInstalled -and -not $Silent) { . "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupWindow.ps1" . "$PSScriptRoot/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1" . "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupDialog.ps1" +. "$PSScriptRoot/Scripts/GUI/MainWindow-WindowChrome.ps1" +. "$PSScriptRoot/Scripts/GUI/MainWindow-AppSelection.ps1" +. "$PSScriptRoot/Scripts/GUI/MainWindow-TweaksBuilder.ps1" +. "$PSScriptRoot/Scripts/GUI/MainWindow-Navigation.ps1" +. "$PSScriptRoot/Scripts/GUI/MainWindow-Deployment.ps1" . "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1" . "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1" . "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1" @@ -374,6 +378,7 @@ $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\Current $script:ModernStandbySupported = CheckModernStandbySupport $script:Params = $PSBoundParameters +$script:UndoParams = @{} # Add default Apps parameter when RemoveApps is requested and Apps was not explicitly provided if ((-not $script:Params.ContainsKey("Apps")) -and $script:Params.ContainsKey("RemoveApps")) {