Skip to content

Commit

Permalink
Merge pull request #208 from berkekbgz/context-menu
Browse files Browse the repository at this point in the history
Feat: Context menu extension
  • Loading branch information
YehudaKremer authored Jul 19, 2023
2 parents 3e20bde + 2780055 commit 404078a
Show file tree
Hide file tree
Showing 16 changed files with 837 additions and 8 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ See [Configurations Examples And Use Cases].
| `os_min_version` | `--os-min-version` | Set minimum OS version, default is `10.0.17763.0` | `10.0.17763.0` |
| [Toast Notifications configuration] | | | |
| [Startup Task configuration] | | pass the app values (args) on startup or user log-in | |
| [Context Menu configuration] | | Use your context menu dll with your app | |

</details>

Expand Down Expand Up @@ -229,4 +230,5 @@ Tags: `msi` `windows` `win10` `win11` `windows10` `windows11` `windows store` `w
[see how the msix version is determined]: https://github.com/YehudaKremer/msix/blob/main/doc/msix_version.md
[toast notifications configuration]: https://github.com/YehudaKremer/msix/blob/main/doc/toast_notifications_configuration.md
[startup task configuration]: https://github.com/YehudaKremer/msix/blob/main/doc/startup_task_configuration.md
[context menu configuration]: https://github.com/YehudaKremer/msix/blob/main/doc/context_menu_configuration.md
[apps for websites]: https://docs.microsoft.com/en-us/windows/uwp/launch-resume/web-to-app-linking
471 changes: 471 additions & 0 deletions doc/context_menu_configuration.md

Large diffs are not rendered by default.

Binary file added doc/context_menu_images/build_configuration.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/context_menu_images/context_menu_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/context_menu_images/dynamic_link_project.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/context_menu_images/nuget.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions lib/msix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ class Msix {
await assets.createIcons();
await assets.copyVCLibsFiles();

if (_config.contextMenuConfiguration?.comSurrogateServers.isNotEmpty ==
true) {
for (var element
in _config.contextMenuConfiguration!.comSurrogateServers) {
await assets.copyContextMenuDll(element.dllPath);
}
}

if (_config.signMsix && !_config.store) {
await SignTool().getCertificatePublisher();
}
Expand Down
41 changes: 35 additions & 6 deletions lib/src/appx_manifest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class AppxManifest {
xmlns:iot="http://schemas.microsoft.com/appx/manifest/iot/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2"
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:rescap3="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/3"
Expand Down Expand Up @@ -90,14 +92,17 @@ class AppxManifest {
(_config.appUriHandlerHosts != null &&
_config.appUriHandlerHosts!.isNotEmpty) ||
_config.enableAtStartup ||
_config.startupTask != null) {
_config.startupTask != null ||
_config.contextMenuConfiguration != null) {
return '''<Extensions>
${!_config.executionAlias.isNull ? _getExecutionAliasExtension() : ''}
${_config.protocolActivation.isNotEmpty ? _getProtocolActivationExtension() : ''}
${!_config.fileExtension.isNull ? _getFileAssociationsExtension() : ''}
${!_config.toastActivatorCLSID.isNull ? _getToastNotificationActivationExtension() : ''}
${_config.enableAtStartup || _config.startupTask != null ? _getStartupTaskExtension() : ''}
${_config.appUriHandlerHosts != null && _config.appUriHandlerHosts!.isNotEmpty ? _getAppUriHandlerHostExtension() : ''}
${_config.contextMenuConfiguration != null ? _getContextMenuExtension() : ''}
${_config.contextMenuConfiguration?.comSurrogateServers.isNotEmpty == true || _config.toastActivatorCLSID != null ? _getComServers() : ''}
</Extensions>''';
} else {
return '';
Expand Down Expand Up @@ -127,6 +132,21 @@ class AppxManifest {
return protocolsActivation;
}

/// Add extension section for context menu
String _getContextMenuExtension() {
return ''' <desktop4:Extension Category="windows.fileExplorerContextMenus">
<desktop4:FileExplorerContextMenus>
${_config.contextMenuConfiguration!.items.map((item) {
return '''<desktop5:ItemType Type="${item.type}">
${item.commands.map((command) {
return '''<desktop5:Verb Id="${command.id.toHtmlEscape()}" Clsid="${command.clsid.toHtmlEscape()}" />''';
}).join('\n ')}
</desktop5:ItemType>''';
}).join('\n ')}
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>''';
}

/// Add extension section for [_config.fileExtension]
String _getFileAssociationsExtension() {
return ''' <uap:Extension Category="windows.fileTypeAssociation">
Expand All @@ -142,16 +162,25 @@ class AppxManifest {

/// Add extension section for "toast_activator" configurations
String _getToastNotificationActivationExtension() {
return ''' <desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="${_config.toastActivatorCLSID.toHtmlEscape()}"/>
</desktop:Extension>''';
}

String _getComServers() {
return ''' <com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="${_config.executableFileName.toHtmlEscape()}" Arguments="${_config.toastActivatorArguments.toHtmlEscape()}" DisplayName="${_config.toastActivatorDisplayName.toHtmlEscape()}">
${_config.toastActivatorCLSID != null ? '''<com:ExeServer Executable="${_config.executableFileName.toHtmlEscape()}" Arguments="${_config.toastActivatorArguments.toHtmlEscape()}" DisplayName="${_config.toastActivatorDisplayName.toHtmlEscape()}">
<com:Class Id="${_config.toastActivatorCLSID.toHtmlEscape()}"/>
</com:ExeServer>
</com:ExeServer>''' : ''}
${_config.contextMenuConfiguration?.comSurrogateServers.map((item) {
return '''<com:SurrogateServer DisplayName="Context menu verb handler">
<com:Class Id="${item.clsid}" Path="${p.basename(item.dllPath).toHtmlEscape()}" ThreadingModel="STA"/>
</com:SurrogateServer>''';
}).join('\n ') ?? ''}
</com:ComServer>
</com:Extension>
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="${_config.toastActivatorCLSID.toHtmlEscape()}"/>
</desktop:Extension>''';
''';
}

String _getStartupTaskExtension() {
Expand Down
13 changes: 12 additions & 1 deletion lib/src/assets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,13 @@ class Assets {
.copyDirectory(Directory(_config.buildFilesFolder));
}

Future<void> copyContextMenuDll(String dllPath) async {
_logger.trace('copying context menu dll');

await File(dllPath)
.copy(p.join(_config.buildFilesFolder, p.basename(dllPath)));
}

/// Clear the build folder from temporary files
Future<void> cleanTemporaryFiles({clearMsixFiles = false}) async {
_logger.trace('cleaning temporary files');
Expand All @@ -285,7 +292,11 @@ class Assets {
'resources.scale-400.pri',
'msvcp140.dll',
'vcruntime140_1.dll',
'vcruntime140.dll'
'vcruntime140.dll',
..._config.contextMenuConfiguration?.comSurrogateServers
.map((server) => basename(server.dllPath))
.toList() ??
[]
].map((fileName) async =>
await File(p.join(buildPath, fileName)).deleteIfExists()),
Directory(p.join(buildPath, 'Images')).deleteIfExists(recursive: true),
Expand Down
78 changes: 77 additions & 1 deletion lib/src/configuration.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:io';
import 'package:msix/src/context_menu_configuration.dart';
import 'package:path/path.dart' as p;
import 'package:args/args.dart';
import 'package:cli_util/cli_logging.dart';
Expand Down Expand Up @@ -68,6 +69,7 @@ class Configuration {
String pubspecYamlPath = "pubspec.yaml";
String osMinVersion = '10.0.17763.0';
bool isTestCertificate = false;
ContextMenuConfiguration? contextMenuConfiguration;

Configuration(this._arguments);

Expand Down Expand Up @@ -184,6 +186,17 @@ class Configuration {
?.toString()
.toLowerCase() ==
'true';

// context menu configurations
dynamic contextMenuYaml = yaml['context_menu'];

bool skipContextMenu = _args.wasParsed('skip-context-menu');

contextMenuConfiguration = contextMenuYaml != null &&
contextMenuYaml is YamlMap &&
!skipContextMenu
? ContextMenuConfiguration.fromYaml(contextMenuYaml)
: null;
}

/// Validate the configuration values and set default values
Expand Down Expand Up @@ -275,6 +288,68 @@ class Configuration {
if (!['x64', 'arm64'].contains(architecture)) {
throw 'Architecture can be "x64" or "arm64", check "msix_config: architecture" at pubspec.yaml';
}

if (contextMenuConfiguration != null) {
if (!await File(contextMenuConfiguration!.dllPath).exists()) {
throw 'The context menu dll file not found in: ${contextMenuConfiguration!.dllPath}, check "msix_config: context_menu: dll_path" at pubspec.yaml';
}

if (contextMenuConfiguration!.items.isEmpty) {
throw 'Context menu items is empty, check "msix_config: context_menu: items" at pubspec.yaml';
}

for (var item in contextMenuConfiguration!.items) {
if (item.type.isNullOrEmpty) {
throw 'Context menu item type is empty';
}

if (item.commands.isEmpty) {
throw 'Context menu item commands is empty';
}

if (contextMenuConfiguration!.items
.where((element) => element.type == item.type)
.length >
1) {
throw 'Found same context menu item type more than once, type must be unique for each item. Type: ${item.type}';
}

for (var command in item.commands) {
if (command.id.isNullOrEmpty) {
throw 'Context menu command id is empty';
}

if (command.clsid.isNullOrEmpty) {
throw 'Context menu command clsid is empty';
}

if (command.customDllPath != null &&
!await File(command.customDllPath!).exists()) {
throw 'The context menu command custom dll file not found in: ${command.customDllPath}, check "msix_config: context_menu: items: commands: custom_dll" at pubspec.yaml';
}

if (command.clsid == toastActivatorCLSID) {
throw 'Context menu command clsid cannot be the same as toast activator clsid, Clsid: ${command.clsid}';
}

if (item.commands
.where((element) => element.clsid == command.clsid)
.length >
1) {
throw 'Found same context menu command more than once in same type. Clsid: ${command.clsid}, Type: ${item.type}';
}

for (List<ContextMenuItemCommand> command2
in contextMenuConfiguration!.items.map((e) => e.commands)) {
if (command2.any((element) =>
element.clsid == command.clsid &&
element.customDllPath != command.customDllPath)) {
throw 'Context menu command clsid must be unique for each class, but found duplicate.\nClsid: ${command.clsid} with different dll path: ${command.customDllPath ?? contextMenuConfiguration!.dllPath} and ${command2.firstWhere((element) => element.id == command.id).customDllPath}';
}
}
}
}
}
}

/// Validate "flutter build windows" output files
Expand Down Expand Up @@ -337,7 +412,8 @@ class Configuration {
..addFlag('automatic-background-task')
..addFlag('update-blocks-activation')
..addFlag('show-prompt')
..addFlag('force-update-from-any-version');
..addFlag('force-update-from-any-version')
..addFlag('skip-context-menu');

// exclude -v (verbose) from the arguments
_args = parser.parse(args.where((arg) => arg != '-v'));
Expand Down
87 changes: 87 additions & 0 deletions lib/src/context_menu_configuration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import 'package:msix/src/method_extensions.dart';
import 'package:yaml/yaml.dart';

class ContextMenuConfiguration {
final String dllPath;
List<ContextMenuItem> items;

ContextMenuConfiguration({required this.dllPath, required this.items});

List<ContextMenuComSurrogateServer> get comSurrogateServers {
return items
.map((e) => e.commands)
.expand((e) => e)
.map((e) {
return ContextMenuComSurrogateServer(
clsid: e.clsid, dllPath: e.customDllPath ?? dllPath);
})
.toList()
.unique((element) => element.clsid);
}

factory ContextMenuConfiguration.fromYaml(YamlMap json) {
return ContextMenuConfiguration(
dllPath: json['dll_path'],
items: (json['items'] as YamlList)
.map((e) => ContextMenuItem.fromYaml(e))
.toList());
}

@override
String toString() {
return 'ContextMenuConfiguration{dllPath: $dllPath, items: $items}';
}
}

class ContextMenuItem {
String type;
List<ContextMenuItemCommand> commands;

ContextMenuItem({required this.type, required this.commands});

factory ContextMenuItem.fromYaml(YamlMap json) {
return ContextMenuItem(
type: json['type'],
commands: (json['commands'] as YamlList)
.map((e) => ContextMenuItemCommand.fromYaml(e))
.toList());
}

@override
String toString() {
return 'ContextMenuItem{type: $type, commands: $commands}';
}
}

class ContextMenuItemCommand {
final String id;
final String clsid;
final String? customDllPath;

ContextMenuItemCommand(
{required this.id, required this.clsid, this.customDllPath});

factory ContextMenuItemCommand.fromYaml(YamlMap json) {
return ContextMenuItemCommand(
id: json['id'],
clsid: json['clsid'],
customDllPath: json['custom_dll']);
}

@override
String toString() {
return 'ContextMenuItemCommand{id: $id, clsid: $clsid, customDllPath: $customDllPath}';
}
}

class ContextMenuComSurrogateServer {
final String clsid;
final String dllPath;

ContextMenuComSurrogateServer({required this.clsid, required this.dllPath});

@override
String toString() {
return 'ContextMenuComSurrogateServer{clsid: $clsid, dllPath: $dllPath}';
}
}
9 changes: 9 additions & 0 deletions lib/src/method_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,12 @@ extension ProcessResultExtensions on ProcessResult {
}
}
}

extension UniqueList<E, Id> on List<E> {
List<E> unique([Id Function(E element)? id, bool inplace = true]) {
final ids = <dynamic>{};
var list = inplace ? this : List<E>.from(this);
list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id));
return list;
}
}
Loading

0 comments on commit 404078a

Please sign in to comment.