blob: 7dd9c1fc4f1672771e36a5c9def9b7b17d1a8d1c [file] [log] [blame]
// Copyright 2023 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
import 'api.dart';
/// Data model for a devtools extension event that will be sent and received
/// over 'postMessage' between DevTools and an embedded extension iFrame.
///
/// See [DevToolsExtensionEventType] for different types of events that are
/// supported over this communication channel.
class DevToolsExtensionEvent {
DevToolsExtensionEvent(
this.type, {
this.data,
this.source,
});
factory DevToolsExtensionEvent.parse(Map<String, Object?> json) {
final eventType =
DevToolsExtensionEventType.from(json[_typeKey]! as String);
final data = (json[_dataKey] as Map?)?.cast<String, Object?>();
final source = json[sourceKey] as String?;
return DevToolsExtensionEvent(eventType, data: data, source: source);
}
static DevToolsExtensionEvent? tryParse(Object data) {
try {
final dataAsMap = (data as Map).cast<String, Object?>();
return DevToolsExtensionEvent.parse(dataAsMap);
} catch (_) {
return null;
}
}
static const _typeKey = 'type';
static const _dataKey = 'data';
static const sourceKey = 'source';
final DevToolsExtensionEventType type;
final Map<String, Object?>? data;
/// Optional field to describe the source that created and sent this event.
final String? source;
Map<String, Object?> toJson() {
return {
_typeKey: type.name,
if (data != null) _dataKey: data!,
};
}
@override
String toString() {
return '[$type, data: ${data.toString()}'
'${source != null ? ', source: $source' : ''}]';
}
}
/// A void callback that handles a [DevToolsExtensionEvent].
typedef ExtensionEventHandler = void Function(DevToolsExtensionEvent event);
/// An extension event of type [DevToolsExtensionEventType.showNotification]
/// that is sent from an extension to DevTools asking DevTools to post a
/// notification the the DevTools notification framework.
class ShowNotificationExtensionEvent extends DevToolsExtensionEvent {
ShowNotificationExtensionEvent({required String message})
: super(
DevToolsExtensionEventType.showNotification,
data: {_messageKey: message},
);
factory ShowNotificationExtensionEvent.from(DevToolsExtensionEvent event) {
assert(event.type == DevToolsExtensionEventType.showNotification);
final message = event.data!.checkValid<String>(_messageKey);
return ShowNotificationExtensionEvent(message: message);
}
static const _messageKey = 'message';
String get message => data![_messageKey] as String;
}
/// An extension event of type [DevToolsExtensionEventType.showBannerMessage]
/// that is sent from an extension to DevTools asking DevTools to post a
/// banner message to the extension's screen using the DevTools banner message
/// framework.
class ShowBannerMessageExtensionEvent extends DevToolsExtensionEvent {
ShowBannerMessageExtensionEvent({
required String id,
required String bannerMessageType,
required String message,
required String extensionName,
bool ignoreIfAlreadyDismissed = true,
bool dismissOnConnectionChanges = true,
}) : assert(bannerMessageType == 'warning' || bannerMessageType == 'error'),
super(
DevToolsExtensionEventType.showBannerMessage,
data: {
_idKey: id,
_bannerMessageTypeKey: bannerMessageType,
_messageKey: message,
_extensionNameKey: extensionName,
_ignoreIfAlreadyDismissedKey: ignoreIfAlreadyDismissed,
_dismissOnConnectionChangesKey: dismissOnConnectionChanges,
},
);
factory ShowBannerMessageExtensionEvent.from(DevToolsExtensionEvent event) {
assert(event.type == DevToolsExtensionEventType.showBannerMessage);
final eventData = event.data!;
final id = eventData.checkValid<String>(_idKey);
final message = eventData.checkValid<String>(_messageKey);
final type = eventData.checkValid<String>(_bannerMessageTypeKey);
final extensionName = eventData.checkValid<String>(_extensionNameKey);
final ignoreIfAlreadyDismissed =
(eventData[_ignoreIfAlreadyDismissedKey] as bool?) ?? true;
final dismissOnConnectionChanges =
(eventData[_dismissOnConnectionChangesKey] as bool?) ?? true;
return ShowBannerMessageExtensionEvent(
id: id,
bannerMessageType: type,
message: message,
extensionName: extensionName,
ignoreIfAlreadyDismissed: ignoreIfAlreadyDismissed,
dismissOnConnectionChanges: dismissOnConnectionChanges,
);
}
static const _messageKey = 'message';
static const _idKey = 'id';
static const _bannerMessageTypeKey = 'bannerMessageType';
static const _extensionNameKey = 'extensionName';
static const _ignoreIfAlreadyDismissedKey = 'ignoreIfAlreadyDismissed';
static const _dismissOnConnectionChangesKey = 'dismissOnConnectionChanges';
String get messageId => data![_idKey] as String;
String get bannerMessageType => data![_bannerMessageTypeKey] as String;
String get message => data![_messageKey] as String;
String get extensionName => data![_extensionNameKey] as String;
bool get ignoreIfAlreadyDismissed =>
(data![_ignoreIfAlreadyDismissedKey] as bool?) ?? true;
/// Whether this message should be dismissed on app connection changes.
bool get dismissOnConnectionChanges =>
(data![_dismissOnConnectionChangesKey] as bool?) ?? true;
}
/// An extension event of type [DevToolsExtensionEventType.copyToClipboard]
/// that is sent from an extension to DevTools asking DevTools copy content to
/// the user's clipboard.
class CopyToClipboardExtensionEvent extends DevToolsExtensionEvent {
CopyToClipboardExtensionEvent({
required String content,
String successMessage = defaultSuccessMessage,
}) : super(
DevToolsExtensionEventType.copyToClipboard,
data: {
_contentKey: content,
_successMessageKey: successMessage,
},
);
factory CopyToClipboardExtensionEvent.from(DevToolsExtensionEvent event) {
assert(event.type == DevToolsExtensionEventType.copyToClipboard);
final content = event.data!.checkValid<String>(_contentKey);
final successMessage = event.data!.checkValid<String>(_successMessageKey);
return CopyToClipboardExtensionEvent(
content: content,
successMessage: successMessage,
);
}
static const _contentKey = 'content';
static const _successMessageKey = 'successMessage';
static const defaultSuccessMessage = 'Copied to clipboard';
String get content => data![_contentKey] as String;
String get successMessage => data![_successMessageKey] as String;
}
extension ParseExtension on Map<String, Object?> {
T checkValid<T>(String key) {
final element = this[key];
if (element == null) {
throw FormatException("Missing key '$key'");
}
if (element is! T) {
throw FormatException(
'Expected element of type $T but got element of type '
'${element.runtimeType}.',
);
}
return element as T;
}
}