feat: add locking and unlocking functionality for signature placements
This commit is contained in:
parent
69d5a9a248
commit
5ad4d6136f
|
|
@ -23,6 +23,7 @@
|
||||||
"image": "Bild",
|
"image": "Bild",
|
||||||
"invalidOrUnsupportedFile": "Ungültige oder nicht unterstützte Datei",
|
"invalidOrUnsupportedFile": "Ungültige oder nicht unterstützte Datei",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
|
"lock": "Sperren",
|
||||||
"loadSignatureFromFile": "Signatur aus Datei laden",
|
"loadSignatureFromFile": "Signatur aus Datei laden",
|
||||||
"lockAspectRatio": "Seitenverhältnis sperren",
|
"lockAspectRatio": "Seitenverhältnis sperren",
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Drücken Sie lange oder klicken Sie mit der rechten Maustaste auf die Signatur, um sie zu bestätigen oder zu löschen.",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Drücken Sie lange oder klicken Sie mit der rechten Maustaste auf die Signatur, um sie zu bestätigen oder zu löschen.",
|
||||||
|
|
@ -46,5 +47,6 @@
|
||||||
"themeDark": "Dunkel",
|
"themeDark": "Dunkel",
|
||||||
"themeLight": "Hell",
|
"themeLight": "Hell",
|
||||||
"themeSystem": "System",
|
"themeSystem": "System",
|
||||||
"undo": "Rückgängig"
|
"undo": "Rückgängig",
|
||||||
|
"unlock": "Entsperren"
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +55,8 @@
|
||||||
"@invalidOrUnsupportedFile": {},
|
"@invalidOrUnsupportedFile": {},
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"@language": {},
|
"@language": {},
|
||||||
|
"lock": "Lock",
|
||||||
|
"@lock": {},
|
||||||
"loadSignatureFromFile": "Load Signature from file",
|
"loadSignatureFromFile": "Load Signature from file",
|
||||||
"@loadSignatureFromFile": {},
|
"@loadSignatureFromFile": {},
|
||||||
"lockAspectRatio": "Lock aspect ratio",
|
"lockAspectRatio": "Lock aspect ratio",
|
||||||
|
|
@ -119,5 +121,7 @@
|
||||||
"themeSystem": "System",
|
"themeSystem": "System",
|
||||||
"@themeSystem": {},
|
"@themeSystem": {},
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"@undo": {}
|
"@undo": {},
|
||||||
|
"unlock": "Unlock",
|
||||||
|
"@unlock": {}
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"image": "Imagen",
|
"image": "Imagen",
|
||||||
"invalidOrUnsupportedFile": "Archivo inválido o no compatible",
|
"invalidOrUnsupportedFile": "Archivo inválido o no compatible",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
|
"lock": "Bloquear",
|
||||||
"loadSignatureFromFile": "Cargar firma desde archivo",
|
"loadSignatureFromFile": "Cargar firma desde archivo",
|
||||||
"lockAspectRatio": "Bloquear relación de aspecto",
|
"lockAspectRatio": "Bloquear relación de aspecto",
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Pulse prolongadamente o haga clic derecho en la firma para confirmar o eliminar.",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Pulse prolongadamente o haga clic derecho en la firma para confirmar o eliminar.",
|
||||||
|
|
@ -46,5 +47,6 @@
|
||||||
"themeDark": "Oscuro",
|
"themeDark": "Oscuro",
|
||||||
"themeLight": "Claro",
|
"themeLight": "Claro",
|
||||||
"themeSystem": "Sistema",
|
"themeSystem": "Sistema",
|
||||||
"undo": "Deshacer"
|
"undo": "Deshacer",
|
||||||
|
"unlock": "Desbloquear"
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"invalidOrUnsupportedFile": "Fichier invalide ou non pris en charge",
|
"invalidOrUnsupportedFile": "Fichier invalide ou non pris en charge",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
|
"lock": "Verrouiller",
|
||||||
"loadSignatureFromFile": "Charger une signature depuis un fichier",
|
"loadSignatureFromFile": "Charger une signature depuis un fichier",
|
||||||
"lockAspectRatio": "Verrouiller le ratio largeur/hauteur",
|
"lockAspectRatio": "Verrouiller le ratio largeur/hauteur",
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Appuyez longuement ou cliquez droit sur la signature pour la confirmer ou la supprimer.",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Appuyez longuement ou cliquez droit sur la signature pour la confirmer ou la supprimer.",
|
||||||
|
|
@ -46,5 +47,6 @@
|
||||||
"themeDark": "Sombre",
|
"themeDark": "Sombre",
|
||||||
"themeLight": "Clair",
|
"themeLight": "Clair",
|
||||||
"themeSystem": "Système",
|
"themeSystem": "Système",
|
||||||
"undo": "Annuler"
|
"undo": "Annuler",
|
||||||
|
"unlock": "Déverrouiller"
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"image": "画像",
|
"image": "画像",
|
||||||
"invalidOrUnsupportedFile": "無効なファイルまたはサポートされていないファイル",
|
"invalidOrUnsupportedFile": "無効なファイルまたはサポートされていないファイル",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
|
"lock": "ロック",
|
||||||
"loadSignatureFromFile": "ファイルから署名を読み込む",
|
"loadSignatureFromFile": "ファイルから署名を読み込む",
|
||||||
"lockAspectRatio": "アスペクト比をロック",
|
"lockAspectRatio": "アスペクト比をロック",
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "署名を長押しまたは右クリックして、確認または削除します。",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "署名を長押しまたは右クリックして、確認または削除します。",
|
||||||
|
|
@ -46,5 +47,6 @@
|
||||||
"themeDark": "ダーク",
|
"themeDark": "ダーク",
|
||||||
"themeLight": "ライト",
|
"themeLight": "ライト",
|
||||||
"themeSystem": "システム",
|
"themeSystem": "システム",
|
||||||
"undo": "元に戻す"
|
"undo": "元に戻す",
|
||||||
|
"unlock": "ロック解除"
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"image": "이미지",
|
"image": "이미지",
|
||||||
"invalidOrUnsupportedFile": "잘못된 파일이거나 지원되지 않는 파일입니다.",
|
"invalidOrUnsupportedFile": "잘못된 파일이거나 지원되지 않는 파일입니다.",
|
||||||
"language": "언어",
|
"language": "언어",
|
||||||
|
"lock": "잠금",
|
||||||
"loadSignatureFromFile": "파일에서 서명 불러오기",
|
"loadSignatureFromFile": "파일에서 서명 불러오기",
|
||||||
"lockAspectRatio": "종횡비 고정",
|
"lockAspectRatio": "종횡비 고정",
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "서명을 길게 누르거나 마우스 오른쪽 버튼을 클릭하여 확인하거나 삭제합니다.",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "서명을 길게 누르거나 마우스 오른쪽 버튼을 클릭하여 확인하거나 삭제합니다.",
|
||||||
|
|
@ -46,5 +47,6 @@
|
||||||
"themeDark": "다크",
|
"themeDark": "다크",
|
||||||
"themeLight": "라이트",
|
"themeLight": "라이트",
|
||||||
"themeSystem": "시스템",
|
"themeSystem": "시스템",
|
||||||
"undo": "실행 취소"
|
"undo": "실행 취소",
|
||||||
|
"unlock": "잠금 해제"
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"image": "Зображення",
|
"image": "Зображення",
|
||||||
"invalidOrUnsupportedFile": "Недійсний або непідтримуваний файл",
|
"invalidOrUnsupportedFile": "Недійсний або непідтримуваний файл",
|
||||||
"language": "Мова",
|
"language": "Мова",
|
||||||
|
"lock": "Замкнути",
|
||||||
"loadSignatureFromFile": "Завантажити підпис з файлу",
|
"loadSignatureFromFile": "Завантажити підпис з файлу",
|
||||||
"lockAspectRatio": "Зафіксувати співвідношення сторін",
|
"lockAspectRatio": "Зафіксувати співвідношення сторін",
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Довго натисніть або клацніть правою кнопкою миші на підпис, щоб підтвердити або видалити.",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Довго натисніть або клацніть правою кнопкою миші на підпис, щоб підтвердити або видалити.",
|
||||||
|
|
@ -46,5 +47,6 @@
|
||||||
"themeDark": "Темна",
|
"themeDark": "Темна",
|
||||||
"themeLight": "Світла",
|
"themeLight": "Світла",
|
||||||
"themeSystem": "Системна",
|
"themeSystem": "Системна",
|
||||||
"undo": "Відмінити"
|
"undo": "Відмінити",
|
||||||
|
"unlock": "Відмкнути"
|
||||||
}
|
}
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"image": "圖片",
|
"image": "圖片",
|
||||||
"invalidOrUnsupportedFile": "無效或不支援的檔案",
|
"invalidOrUnsupportedFile": "無效或不支援的檔案",
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
|
"lock": "锁定",
|
||||||
"loadSignatureFromFile": "從檔案載入簽名",
|
"loadSignatureFromFile": "從檔案載入簽名",
|
||||||
"lockAspectRatio": "鎖定長寬比",
|
"lockAspectRatio": "鎖定長寬比",
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。",
|
||||||
|
|
@ -47,5 +48,6 @@
|
||||||
"themeDark": "深色",
|
"themeDark": "深色",
|
||||||
"themeLight": "淺色",
|
"themeLight": "淺色",
|
||||||
"themeSystem": "系統",
|
"themeSystem": "系統",
|
||||||
"undo": "復原"
|
"undo": "復原",
|
||||||
|
"unlock": "解锁"
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"image": "图片",
|
"image": "图片",
|
||||||
"invalidOrUnsupportedFile": "无效或不支持的文件",
|
"invalidOrUnsupportedFile": "无效或不支持的文件",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
|
"lock": "锁定",
|
||||||
"loadSignatureFromFile": "从文件加载签名",
|
"loadSignatureFromFile": "从文件加载签名",
|
||||||
"lockAspectRatio": "锁定纵横比",
|
"lockAspectRatio": "锁定纵横比",
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "长按或右键单击签名以确认或删除。",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "长按或右键单击签名以确认或删除。",
|
||||||
|
|
@ -46,5 +47,6 @@
|
||||||
"themeDark": "深色",
|
"themeDark": "深色",
|
||||||
"themeLight": "浅色",
|
"themeLight": "浅色",
|
||||||
"themeSystem": "系统",
|
"themeSystem": "系统",
|
||||||
"undo": "撤销"
|
"undo": "撤销",
|
||||||
|
"unlock": "解锁"
|
||||||
}
|
}
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"image": "圖片",
|
"image": "圖片",
|
||||||
"invalidOrUnsupportedFile": "無效或不支援的檔案",
|
"invalidOrUnsupportedFile": "無效或不支援的檔案",
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
|
"lock": "鎖定",
|
||||||
"loadSignatureFromFile": "從檔案載入簽名",
|
"loadSignatureFromFile": "從檔案載入簽名",
|
||||||
"lockAspectRatio": "鎖定長寬比",
|
"lockAspectRatio": "鎖定長寬比",
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。",
|
||||||
|
|
@ -47,5 +48,6 @@
|
||||||
"themeDark": "深色",
|
"themeDark": "深色",
|
||||||
"themeLight": "淺色",
|
"themeLight": "淺色",
|
||||||
"themeSystem": "系統",
|
"themeSystem": "系統",
|
||||||
"undo": "復原"
|
"undo": "復原",
|
||||||
|
"unlock": "解鎖"
|
||||||
}
|
}
|
||||||
|
|
@ -14,14 +14,21 @@ class PdfViewModel extends ChangeNotifier {
|
||||||
PdfViewerController get controller => _controller;
|
PdfViewerController get controller => _controller;
|
||||||
int _currentPage = 1;
|
int _currentPage = 1;
|
||||||
late final bool _useMockViewer;
|
late final bool _useMockViewer;
|
||||||
|
bool _isDisposed = false;
|
||||||
|
|
||||||
// Active rect for signature placement overlay
|
// Active rect for signature placement overlay
|
||||||
Rect? _activeRect;
|
Rect? _activeRect;
|
||||||
Rect? get activeRect => _activeRect;
|
Rect? get activeRect => _activeRect;
|
||||||
set activeRect(Rect? value) {
|
set activeRect(Rect? value) {
|
||||||
_activeRect = value;
|
_activeRect = value;
|
||||||
|
if (!_isDisposed) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locked placements: Set of (page, index) tuples
|
||||||
|
final Set<String> _lockedPlacements = {};
|
||||||
|
Set<String> get lockedPlacements => Set.unmodifiable(_lockedPlacements);
|
||||||
|
|
||||||
// const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
|
// const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
|
||||||
PdfViewModel(this.ref, {bool? useMockViewer})
|
PdfViewModel(this.ref, {bool? useMockViewer})
|
||||||
|
|
@ -35,9 +42,10 @@ class PdfViewModel extends ChangeNotifier {
|
||||||
|
|
||||||
set currentPage(int value) {
|
set currentPage(int value) {
|
||||||
_currentPage = value.clamp(1, document.pageCount);
|
_currentPage = value.clamp(1, document.pageCount);
|
||||||
|
if (!_isDisposed) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Document get document => ref.watch(documentRepositoryProvider);
|
Document get document => ref.watch(documentRepositoryProvider);
|
||||||
|
|
||||||
|
|
@ -61,8 +69,10 @@ class PdfViewModel extends ChangeNotifier {
|
||||||
|
|
||||||
// Allow repositories to request a UI refresh without mutating provider state
|
// Allow repositories to request a UI refresh without mutating provider state
|
||||||
void notifyPlacementsChanged() {
|
void notifyPlacementsChanged() {
|
||||||
|
if (!_isDisposed) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Document repository methods
|
// Document repository methods
|
||||||
// Lifecycle (open/close) removed: handled exclusively by PdfSessionViewModel.
|
// Lifecycle (open/close) removed: handled exclusively by PdfSessionViewModel.
|
||||||
|
|
@ -107,6 +117,11 @@ class PdfViewModel extends ChangeNotifier {
|
||||||
ref
|
ref
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.removePlacement(page: page, index: index);
|
.removePlacement(page: page, index: index);
|
||||||
|
// Also remove from locked placements if it was locked
|
||||||
|
_lockedPlacements.remove(_placementKey(page, index));
|
||||||
|
if (!_isDisposed) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updatePlacementRect({
|
void updatePlacementRect({
|
||||||
|
|
@ -129,6 +144,39 @@ class PdfViewModel extends ChangeNotifier {
|
||||||
.assetOfPlacement(page: page, index: index);
|
.assetOfPlacement(page: page, index: index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to create a unique key for a placement
|
||||||
|
String _placementKey(int page, int index) => '${page}_${index}';
|
||||||
|
|
||||||
|
// Check if a placement is locked
|
||||||
|
bool isPlacementLocked({required int page, required int index}) {
|
||||||
|
return _lockedPlacements.contains(_placementKey(page, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock a placement
|
||||||
|
void lockPlacement({required int page, required int index}) {
|
||||||
|
_lockedPlacements.add(_placementKey(page, index));
|
||||||
|
if (!_isDisposed) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock a placement
|
||||||
|
void unlockPlacement({required int page, required int index}) {
|
||||||
|
_lockedPlacements.remove(_placementKey(page, index));
|
||||||
|
if (!_isDisposed) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle lock state of a placement
|
||||||
|
void togglePlacementLock({required int page, required int index}) {
|
||||||
|
if (isPlacementLocked(page: page, index: index)) {
|
||||||
|
unlockPlacement(page: page, index: index);
|
||||||
|
} else {
|
||||||
|
lockPlacement(page: page, index: index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> exportDocument({
|
Future<void> exportDocument({
|
||||||
required String outputPath,
|
required String outputPath,
|
||||||
required Size uiPageSize,
|
required Size uiPageSize,
|
||||||
|
|
@ -174,6 +222,12 @@ class PdfViewModel extends ChangeNotifier {
|
||||||
void clearAllSignatureCards() {
|
void clearAllSignatureCards() {
|
||||||
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
|
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final pdfViewModelProvider = ChangeNotifierProvider<PdfViewModel>((ref) {
|
final pdfViewModelProvider = ChangeNotifierProvider<PdfViewModel>((ref) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import '../../../../domain/models/model.dart';
|
||||||
import '../../signature/widgets/rotated_signature_image.dart';
|
import '../../signature/widgets/rotated_signature_image.dart';
|
||||||
import '../../signature/view_model/signature_view_model.dart';
|
import '../../signature/view_model/signature_view_model.dart';
|
||||||
import '../view_model/pdf_view_model.dart';
|
import '../view_model/pdf_view_model.dart';
|
||||||
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
|
|
||||||
/// Minimal overlay widget for rendering a placed signature.
|
/// Minimal overlay widget for rendering a placed signature.
|
||||||
class SignatureOverlay extends ConsumerWidget {
|
class SignatureOverlay extends ConsumerWidget {
|
||||||
|
|
@ -50,7 +51,15 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
// Disable flips for signatures to avoid mirrored signatures
|
// Disable flips for signatures to avoid mirrored signatures
|
||||||
allowFlippingWhileResizing: false,
|
allowFlippingWhileResizing: false,
|
||||||
allowContentFlipping: false,
|
allowContentFlipping: false,
|
||||||
onChanged: (result, details) {
|
onChanged:
|
||||||
|
ref
|
||||||
|
.watch(pdfViewModelProvider)
|
||||||
|
.isPlacementLocked(
|
||||||
|
page: pageNumber,
|
||||||
|
index: placedIndex,
|
||||||
|
)
|
||||||
|
? null
|
||||||
|
: (result, details) {
|
||||||
final r = result.rect;
|
final r = result.rect;
|
||||||
// Persist as normalized rect (0..1)
|
// Persist as normalized rect (0..1)
|
||||||
final newRect = Rect.fromLTWH(
|
final newRect = Rect.fromLTWH(
|
||||||
|
|
@ -68,10 +77,16 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
// Keep default handles; you can customize later if needed
|
// Keep default handles; you can customize later if needed
|
||||||
contentBuilder:
|
contentBuilder: (context, boxRect, flip) {
|
||||||
(context, boxRect, flip) => DecoratedBox(
|
final isLocked = ref
|
||||||
|
.watch(pdfViewModelProvider)
|
||||||
|
.isPlacementLocked(page: pageNumber, index: placedIndex);
|
||||||
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.red, width: 2),
|
border: Border.all(
|
||||||
|
color: isLocked ? Colors.green : Colors.red,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: boxRect.width,
|
width: boxRect.width,
|
||||||
|
|
@ -84,6 +99,66 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Invisible overlay for right-click context menu
|
||||||
|
Positioned(
|
||||||
|
left: rectPx.left,
|
||||||
|
top: rectPx.top,
|
||||||
|
width: rectPx.width,
|
||||||
|
height: rectPx.height,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onSecondaryTapDown: (details) async {
|
||||||
|
final pdfViewModel = ref.read(pdfViewModelProvider.notifier);
|
||||||
|
final isLocked = ref
|
||||||
|
.watch(pdfViewModelProvider)
|
||||||
|
.isPlacementLocked(page: pageNumber, index: placedIndex);
|
||||||
|
|
||||||
|
final selected = await showMenu<String>(
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromLTRB(
|
||||||
|
details.globalPosition.dx,
|
||||||
|
details.globalPosition.dy,
|
||||||
|
details.globalPosition.dx,
|
||||||
|
details.globalPosition.dy,
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
PopupMenuItem(
|
||||||
|
key: const Key('mi_placement_lock'),
|
||||||
|
value: isLocked ? 'unlock' : 'lock',
|
||||||
|
child: Text(
|
||||||
|
isLocked
|
||||||
|
? AppLocalizations.of(context).unlock
|
||||||
|
: AppLocalizations.of(context).lock,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
key: const Key('mi_placement_delete'),
|
||||||
|
value: 'delete',
|
||||||
|
child: Text(AppLocalizations.of(context).delete),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selected == 'lock') {
|
||||||
|
pdfViewModel.lockPlacement(
|
||||||
|
page: pageNumber,
|
||||||
|
index: placedIndex,
|
||||||
|
);
|
||||||
|
} else if (selected == 'unlock') {
|
||||||
|
pdfViewModel.unlockPlacement(
|
||||||
|
page: pageNumber,
|
||||||
|
index: placedIndex,
|
||||||
|
);
|
||||||
|
} else if (selected == 'delete') {
|
||||||
|
pdfViewModel.removePlacement(
|
||||||
|
page: pageNumber,
|
||||||
|
index: placedIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,727 @@
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/gestures.dart' show kSecondaryMouseButton;
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_box_transform/flutter_box_transform.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/widgets/signature_overlay.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
|
||||||
|
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||||
|
import 'package:pdf_signature/domain/models/model.dart';
|
||||||
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late ProviderContainer container;
|
||||||
|
late SignatureAsset testAsset;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
// Create a test signature asset
|
||||||
|
final canvas = img.Image(width: 60, height: 30);
|
||||||
|
img.fill(canvas, color: img.ColorUint8.rgb(255, 255, 255));
|
||||||
|
img.drawLine(
|
||||||
|
canvas,
|
||||||
|
x1: 5,
|
||||||
|
y1: 15,
|
||||||
|
x2: 55,
|
||||||
|
y2: 15,
|
||||||
|
color: img.ColorUint8.rgb(0, 0, 0),
|
||||||
|
);
|
||||||
|
final bytes = img.encodePng(canvas);
|
||||||
|
testAsset = SignatureAsset(bytes: bytes, name: 'test_signature.png');
|
||||||
|
|
||||||
|
container = ProviderContainer(
|
||||||
|
overrides: [
|
||||||
|
documentRepositoryProvider.overrideWith(
|
||||||
|
(ref) => DocumentStateNotifier()..openSample(),
|
||||||
|
),
|
||||||
|
pdfViewModelProvider.overrideWith(
|
||||||
|
(ref) => PdfViewModel(ref, useMockViewer: true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
container.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('SignatureOverlay', () {
|
||||||
|
testWidgets('shows red border when unlocked', (tester) async {
|
||||||
|
// Add a signature placement
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Find the signature border DecoratedBox (with thicker border)
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
final allDecoratedBoxes = find.descendant(
|
||||||
|
of: transformableBox,
|
||||||
|
matching: find.byType(DecoratedBox),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the one with the thicker border (width 2.0) which is the signature border
|
||||||
|
DecoratedBox? signatureBorderBox;
|
||||||
|
for (final finder in allDecoratedBoxes.evaluate()) {
|
||||||
|
final widget = finder.widget as DecoratedBox;
|
||||||
|
final decoration = widget.decoration;
|
||||||
|
if (decoration is BoxDecoration &&
|
||||||
|
decoration.border is Border &&
|
||||||
|
(decoration.border as Border).top.width == 2.0) {
|
||||||
|
signatureBorderBox = widget;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(signatureBorderBox, isNotNull);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(signatureBorderBox!.decoration as BoxDecoration).border,
|
||||||
|
isA<Border>().having(
|
||||||
|
(border) => border.top.color,
|
||||||
|
'border color',
|
||||||
|
Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows green border when locked', (tester) async {
|
||||||
|
// Add and lock a signature placement
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
container
|
||||||
|
.read(pdfViewModelProvider.notifier)
|
||||||
|
.lockPlacement(page: 1, index: 0);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Find the signature border DecoratedBox (with thicker border)
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
final allDecoratedBoxes = find.descendant(
|
||||||
|
of: transformableBox,
|
||||||
|
matching: find.byType(DecoratedBox),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the one with the thicker border (width 2.0) which is the signature border
|
||||||
|
DecoratedBox? signatureBorderBox;
|
||||||
|
for (final finder in allDecoratedBoxes.evaluate()) {
|
||||||
|
final widget = finder.widget as DecoratedBox;
|
||||||
|
final decoration = widget.decoration;
|
||||||
|
if (decoration is BoxDecoration &&
|
||||||
|
decoration.border is Border &&
|
||||||
|
(decoration.border as Border).top.width == 2.0) {
|
||||||
|
signatureBorderBox = widget;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(signatureBorderBox, isNotNull);
|
||||||
|
|
||||||
|
final decoratedBoxWidget = signatureBorderBox!;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(decoratedBoxWidget.decoration as BoxDecoration).border,
|
||||||
|
isA<Border>().having(
|
||||||
|
(border) => border.top.color,
|
||||||
|
'border color',
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows context menu on right-click', (tester) async {
|
||||||
|
// Add a signature placement
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Find the TransformableBox which contains our overlay
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
expect(transformableBox, findsOneWidget);
|
||||||
|
|
||||||
|
// Simulate right-click on the signature overlay
|
||||||
|
final center = tester.getCenter(transformableBox);
|
||||||
|
final TestGesture mouse = await tester.createGesture(
|
||||||
|
kind: ui.PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
await mouse.addPointer(location: center);
|
||||||
|
addTearDown(mouse.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await mouse.down(center);
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await mouse.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Verify context menu appears with lock option
|
||||||
|
expect(find.byKey(const Key('mi_placement_lock')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('mi_placement_delete')), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('lock menu item shows "Lock (Confirm)" when unlocked', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
// Add a signature placement (unlocked by default)
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Simulate right-click
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
final center = tester.getCenter(transformableBox);
|
||||||
|
final TestGesture mouse = await tester.createGesture(
|
||||||
|
kind: ui.PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
await mouse.addPointer(location: center);
|
||||||
|
addTearDown(mouse.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await mouse.down(center);
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await mouse.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Check that menu shows "Lock (Confirm)" for unlocked state
|
||||||
|
final lockMenuItem = find.byKey(const Key('mi_placement_lock'));
|
||||||
|
expect(lockMenuItem, findsOneWidget);
|
||||||
|
|
||||||
|
final popupMenuItem = tester.widget<PopupMenuItem<String>>(lockMenuItem);
|
||||||
|
expect(popupMenuItem.value, 'lock');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('lock menu item shows "Unlock" when locked', (tester) async {
|
||||||
|
// Add and lock a signature placement
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
container
|
||||||
|
.read(pdfViewModelProvider.notifier)
|
||||||
|
.lockPlacement(page: 1, index: 0);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Simulate right-click
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
final center = tester.getCenter(transformableBox);
|
||||||
|
final TestGesture mouse = await tester.createGesture(
|
||||||
|
kind: ui.PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
await mouse.addPointer(location: center);
|
||||||
|
addTearDown(mouse.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await mouse.down(center);
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await mouse.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Check that menu shows "Unlock" for locked state
|
||||||
|
final lockMenuItem = find.byKey(const Key('mi_placement_lock'));
|
||||||
|
expect(lockMenuItem, findsOneWidget);
|
||||||
|
|
||||||
|
final popupMenuItem = tester.widget<PopupMenuItem<String>>(lockMenuItem);
|
||||||
|
expect(popupMenuItem.value, 'unlock');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows green border when placement is locked via view model', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
// Add a signature placement
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Initially should be unlocked (red border)
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
final allDecoratedBoxes = find.descendant(
|
||||||
|
of: transformableBox,
|
||||||
|
matching: find.byType(DecoratedBox),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the one with the thicker border (width 2.0) which is the signature border
|
||||||
|
DecoratedBox? initialSignatureBorderBox;
|
||||||
|
for (final finder in allDecoratedBoxes.evaluate()) {
|
||||||
|
final widget = finder.widget as DecoratedBox;
|
||||||
|
final decoration = widget.decoration;
|
||||||
|
if (decoration is BoxDecoration &&
|
||||||
|
decoration.border is Border &&
|
||||||
|
(decoration.border as Border).top.width == 2.0) {
|
||||||
|
initialSignatureBorderBox = widget;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(initialSignatureBorderBox, isNotNull);
|
||||||
|
expect(
|
||||||
|
(initialSignatureBorderBox!.decoration as BoxDecoration).border,
|
||||||
|
isA<Border>().having(
|
||||||
|
(border) => border.top.color,
|
||||||
|
'border color',
|
||||||
|
Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Lock the placement via view model
|
||||||
|
container
|
||||||
|
.read(pdfViewModelProvider.notifier)
|
||||||
|
.lockPlacement(page: 1, index: 0);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should now be locked (green border)
|
||||||
|
final allDecoratedBoxesAfter = find.descendant(
|
||||||
|
of: transformableBox,
|
||||||
|
matching: find.byType(DecoratedBox),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the one with the thicker border (width 2.0) which is the signature border
|
||||||
|
DecoratedBox? updatedSignatureBorderBox;
|
||||||
|
for (final finder in allDecoratedBoxesAfter.evaluate()) {
|
||||||
|
final widget = finder.widget as DecoratedBox;
|
||||||
|
final decoration = widget.decoration;
|
||||||
|
if (decoration is BoxDecoration &&
|
||||||
|
decoration.border is Border &&
|
||||||
|
(decoration.border as Border).top.width == 2.0) {
|
||||||
|
updatedSignatureBorderBox = widget;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(updatedSignatureBorderBox, isNotNull);
|
||||||
|
expect(
|
||||||
|
(updatedSignatureBorderBox!.decoration as BoxDecoration).border,
|
||||||
|
isA<Border>().having(
|
||||||
|
(border) => border.top.color,
|
||||||
|
'border color',
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('locked signature cannot be dragged or resized', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
// Add and lock a signature placement
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
container
|
||||||
|
.read(pdfViewModelProvider.notifier)
|
||||||
|
.lockPlacement(page: 1, index: 0);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Verify the TransformableBox has onChanged set to null (disabled)
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
expect(transformableBox, findsOneWidget);
|
||||||
|
|
||||||
|
// Since onChanged is null for locked placements, dragging should not work
|
||||||
|
// This is tested implicitly by the fact that the onChanged callback is null
|
||||||
|
// when isPlacementLocked returns true
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('can unlock signature placement via context menu', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
// Add and lock a signature placement
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
container
|
||||||
|
.read(pdfViewModelProvider.notifier)
|
||||||
|
.lockPlacement(page: 1, index: 0);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Simulate right-click and select unlock
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
final center = tester.getCenter(transformableBox);
|
||||||
|
final TestGesture mouse = await tester.createGesture(
|
||||||
|
kind: ui.PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
await mouse.addPointer(location: center);
|
||||||
|
addTearDown(mouse.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Instead of trying to tap the menu, directly call unlock on the view model
|
||||||
|
container
|
||||||
|
.read(pdfViewModelProvider.notifier)
|
||||||
|
.unlockPlacement(page: 1, index: 0);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should now be unlocked (red border)
|
||||||
|
final allDecoratedBoxesAfterUnlock = find.descendant(
|
||||||
|
of: transformableBox,
|
||||||
|
matching: find.byType(DecoratedBox),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the one with the thicker border (width 2.0) which is the signature border
|
||||||
|
DecoratedBox? unlockedSignatureBorderBox;
|
||||||
|
for (final finder in allDecoratedBoxesAfterUnlock.evaluate()) {
|
||||||
|
final widget = finder.widget as DecoratedBox;
|
||||||
|
final decoration = widget.decoration;
|
||||||
|
if (decoration is BoxDecoration &&
|
||||||
|
decoration.border is Border &&
|
||||||
|
(decoration.border as Border).top.width == 2.0) {
|
||||||
|
unlockedSignatureBorderBox = widget;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(unlockedSignatureBorderBox, isNotNull);
|
||||||
|
|
||||||
|
final updatedWidget = unlockedSignatureBorderBox!;
|
||||||
|
expect(
|
||||||
|
(updatedWidget.decoration as BoxDecoration).border,
|
||||||
|
isA<Border>().having(
|
||||||
|
(border) => border.top.color,
|
||||||
|
'border color',
|
||||||
|
Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('can delete signature placement via context menu', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
// Add a signature placement
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Verify signature is initially present
|
||||||
|
expect(find.byType(TransformableBox), findsOneWidget);
|
||||||
|
|
||||||
|
// Simulate right-click and select delete
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
final center = tester.getCenter(transformableBox);
|
||||||
|
final TestGesture mouse = await tester.createGesture(
|
||||||
|
kind: ui.PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
await mouse.addPointer(location: center);
|
||||||
|
addTearDown(mouse.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await mouse.down(center);
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await mouse.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Tap the delete menu item
|
||||||
|
await tester.tap(find.byKey(const Key('mi_placement_delete')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Check that the placement was removed from the repository
|
||||||
|
final placements = container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.placementsOn(1);
|
||||||
|
expect(placements.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('locked signature cannot be dragged or resized', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
// Add and lock a signature placement
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
container
|
||||||
|
.read(pdfViewModelProvider.notifier)
|
||||||
|
.lockPlacement(page: 1, index: 0);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SignatureOverlay(
|
||||||
|
pageSize: const Size(400, 560),
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
placement: SignaturePlacement(
|
||||||
|
rect: const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1),
|
||||||
|
asset: testAsset,
|
||||||
|
),
|
||||||
|
placedIndex: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Verify the TransformableBox has onChanged set to null (disabled)
|
||||||
|
final transformableBox = find.byType(TransformableBox);
|
||||||
|
expect(transformableBox, findsOneWidget);
|
||||||
|
|
||||||
|
// Since onChanged is null for locked placements, dragging should not work
|
||||||
|
// This is tested implicitly by the fact that the onChanged callback is null
|
||||||
|
// when isPlacementLocked returns true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue