feat(ui): ajouter l’aperçu avancé et uniformiser les dialogues
This commit is contained in:
parent
dae5f63932
commit
d7131aa1b6
37 changed files with 1784 additions and 378 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -2,6 +2,18 @@
|
|||
|
||||
### Added
|
||||
|
||||
- OCR d’identité contrôlé et révisable dans la création d’une personne,
|
||||
l’import normal et la fiche de preuve, avec texte brut immuable,
|
||||
transcription corrigée persistante, `manual_override`, `manual_entry` et
|
||||
notes documentaires factuelles.
|
||||
- Consultation complète des données OCR persistées, sélection explicite d’un
|
||||
`OcrRun`, révision sans relance de Tesseract et création d’un nouveau run
|
||||
lors d’une nouvelle analyse.
|
||||
- Aperçu partagé avec zoom, ajustement, défilement bidirectionnel, navigation
|
||||
PDF multipage, compteur de pages et provenance OCR synchronisée.
|
||||
- Politique commune des dialogues GTK métiers complexes : parent transitoire,
|
||||
géométrie responsive, répartition initiale 2/3–1/3, formulaire défilable et
|
||||
barre d’actions fixe.
|
||||
- Préparation du pivot e-mail : pipeline EML asynchrone, extraction MIME
|
||||
sécurisée, propositions bancaires IBAN/BIC et vocabulaire contrôlé.
|
||||
|
||||
|
|
@ -23,6 +35,12 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Rafraîchissement immédiat de Workspace après import ou révision OCR et
|
||||
persistance des données sur l’UUID définitif de la preuve, y compris après
|
||||
fermeture et réouverture de SQLite.
|
||||
- Fermeture sûre du dialogue après enregistrement d’une révision OCR, avec
|
||||
protection contre le double clic, maintien ouvert en cas d’échec et absence
|
||||
de relance de Tesseract.
|
||||
- Conservation du cadrage du graphe pendant les rechargements.
|
||||
- Persistance SQLite du zoom et de la position du graphe entre deux sessions.
|
||||
- Réduction de la fenêtre d’intégration des extractions pour maintenir les
|
||||
|
|
|
|||
17
Makefile
17
Makefile
|
|
@ -164,6 +164,7 @@ TEST_OCR_PROVENANCE_OVERLAY_GTK := tests/test_ocr_provenance_overlay_gtk
|
|||
TEST_EVIDENCE_METADATA_DIALOG_GTK := tests/test_evidence_metadata_dialog_gtk
|
||||
TEST_EVIDENCE_IDENTITY_IMPORT_GTK := tests/test_evidence_identity_import_gtk
|
||||
TEST_WORKSPACE_IDENTITY_OCR_GTK := tests/test_workspace_identity_ocr_gtk
|
||||
TEST_DIALOG_GEOMETRY_GTK := tests/test_dialog_geometry_gtk
|
||||
FAKE_DOCUMENT_TOOL := tests/fake_document_tool
|
||||
|
||||
DOCUMENT_ANALYSIS_TEST_SOURCES := \
|
||||
|
|
@ -178,6 +179,10 @@ $(TEST_DOCUMENT_TOOL_RUNNER): tests/test_document_tool_runner.c \
|
|||
tests/test_document_tool_runner.c \
|
||||
$(DOCUMENT_ANALYSIS_TEST_SOURCES) -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
$(TEST_DIALOG_GEOMETRY_GTK): tests/test_dialog_geometry_gtk.c \
|
||||
src/views/dialog_geometry.c
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TEST_IDENTITY_OCR): tests/test_identity_ocr.c \
|
||||
|
|
@ -571,6 +576,7 @@ $(TEST_EVIDENCE_TYPE_DAO): \
|
|||
$(TEST_EVIDENCE_IMPORT_DIALOG): \
|
||||
tests/test_evidence_import_dialog.c \
|
||||
src/views/evidence_import_dialog.c \
|
||||
src/views/dialog_geometry.c \
|
||||
src/models/evidence_type.c \
|
||||
src/widgets/evidence_preview_widget.c \
|
||||
src/core/evidence_preview_task.c src/core/evidence_preview.c \
|
||||
|
|
@ -883,7 +889,8 @@ $(TEST_PERSON_DIALOG_LIFECYCLE): tests/test_person_dialog_lifecycle.c \
|
|||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
$(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
|
||||
src/views/create_person_dialog.c src/core/person_dialog_lifecycle.c \
|
||||
src/views/create_person_dialog.c src/views/dialog_geometry.c \
|
||||
src/core/person_dialog_lifecycle.c \
|
||||
src/core/person_confirmation_summary.c \
|
||||
src/core/evidence_staging.c src/core/evidence_staging_task.c \
|
||||
src/core/evidence_preview_task.c src/core/evidence_preview.c \
|
||||
|
|
@ -906,7 +913,8 @@ $(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
|
|||
|
||||
$(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \
|
||||
tests/test_create_person_dialog_ocr_gtk.c \
|
||||
src/views/create_person_dialog.c src/core/person_dialog_lifecycle.c \
|
||||
src/views/create_person_dialog.c src/views/dialog_geometry.c \
|
||||
src/core/person_dialog_lifecycle.c \
|
||||
src/core/person_confirmation_summary.c \
|
||||
src/core/evidence_staging.c src/core/evidence_staging_task.c \
|
||||
src/core/evidence_preview_task.c src/core/evidence_preview.c \
|
||||
|
|
@ -1122,7 +1130,8 @@ test: \
|
|||
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
|
||||
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
|
||||
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
|
||||
$(TEST_WORKSPACE_IDENTITY_OCR_GTK)
|
||||
$(TEST_WORKSPACE_IDENTITY_OCR_GTK) \
|
||||
$(TEST_DIALOG_GEOMETRY_GTK)
|
||||
@echo "Exécution des tests..."
|
||||
@./$(TEST_NODE)
|
||||
@./$(TEST_TREE_MODEL)
|
||||
|
|
@ -1216,6 +1225,7 @@ test: \
|
|||
@$(TEST_EVIDENCE_METADATA_DIALOG_GTK)
|
||||
@$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK)
|
||||
@$(TEST_WORKSPACE_IDENTITY_OCR_GTK)
|
||||
@$(TEST_DIALOG_GEOMETRY_GTK)
|
||||
@echo "Tous les tests sont valides."
|
||||
|
||||
%.o: %.c
|
||||
|
|
@ -1313,6 +1323,7 @@ clean:
|
|||
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
|
||||
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
|
||||
$(TEST_WORKSPACE_IDENTITY_OCR_GTK) \
|
||||
$(TEST_DIALOG_GEOMETRY_GTK) \
|
||||
$(FAKE_DOCUMENT_TOOL)
|
||||
|
||||
|
||||
|
|
|
|||
65
README.md
65
README.md
|
|
@ -123,20 +123,30 @@ Les outils externes sont lancés avec `GSubprocess` et des arguments séparés.
|
|||
|
||||
## Fonctionnalités déjà présentes
|
||||
|
||||
La création d’une personne propose une étape facultative d’OCR contrôlé des
|
||||
documents d’identité. Elle ne démarre que sur action explicite, travaille sur
|
||||
une copie vérifiée PNG, JPEG, HEIC, HEIF ou sur une page PDF choisie, et laisse
|
||||
chaque champ à l’état « À vérifier ». L’acceptation, la modification ou le rejet
|
||||
s’effectuent champ par champ, sans verdict d’authenticité, reconnaissance
|
||||
faciale, fusion de personne ni changement automatique du statut
|
||||
d’identification.
|
||||
La création d’une personne et l’import normal d’une preuve proposent un OCR
|
||||
contrôlé des documents d’identité. Il ne démarre que sur action explicite,
|
||||
travaille sur une copie vérifiée PNG, JPEG, HEIC, HEIF ou sur une page PDF
|
||||
choisie, et conserve séparément le texte OCR brut immuable et une transcription
|
||||
corrigée rééditable et réinitialisable. Les propositions restent révisables :
|
||||
elles peuvent être acceptées, corrigées à nouveau, restaurées depuis leur
|
||||
valeur brute ou rejetées. Un champ visible mais omis par l’OCR peut être saisi
|
||||
avec l’origine `manual_entry` ; une correction d’une valeur extraite conserve
|
||||
l’origine `manual_override`. Des notes factuelles peuvent signaler un document
|
||||
tronqué, flou, masqué ou incomplet, sans reconstruire une zone absente.
|
||||
|
||||
Ce parcours est également disponible lors de l’import normal d’une preuve,
|
||||
avec sélection explicite ou présélection de la personne. Il traite une seule
|
||||
preuve par opération. L’import multiple sans OCR reste disponible ; plusieurs
|
||||
preuves doivent être OCRisées successivement, chacune dans son propre
|
||||
parcours. Les dialogues concernés affichent la liste à gauche et un grand
|
||||
aperçu redimensionnable à droite.
|
||||
La fiche directe d’une preuve relit depuis SQLite le texte brut, la
|
||||
transcription corrigée, les exécutions `OcrRun`, observations, champs, notes,
|
||||
artefacts, empreintes SHA-256, personne liée et provenance graphique. Un run
|
||||
est sélectionné explicitement lorsqu’un historique en contient plusieurs.
|
||||
« Réviser l’analyse OCR » modifie uniquement ce run sans relancer Tesseract ;
|
||||
« Relancer une nouvelle analyse » crée un nouveau run sans écraser
|
||||
l’historique.
|
||||
|
||||
L’import multiple sans OCR reste disponible. L’OCR groupé n’est pas pris en
|
||||
charge : les preuves importées ensemble sont ensuite analysées une par une
|
||||
depuis leur fiche, sur leur UUID définitif et sans doublon. Aucun de ces
|
||||
parcours ne produit de verdict d’authenticité, de reconnaissance faciale, de
|
||||
fusion de personne, d’identité certaine ou de rôle d’auteur automatique.
|
||||
|
||||
Le socle actuel comprend notamment :
|
||||
|
||||
|
|
@ -212,8 +222,18 @@ Le socle actuel comprend notamment :
|
|||
définitifs qu’après confirmation globale ;
|
||||
- rattachement transactionnel de toutes les preuves retenues, avec rollback
|
||||
SQLite et suppression compensatoire des copies définitives en cas d’échec.
|
||||
- aperçu asynchrone contrôlé PNG/JPEG, HEIC/HEIF, MP4/MOV, texte, EML et
|
||||
première page PDF, sans OCR ni écriture SQLite ;
|
||||
- aperçu asynchrone contrôlé PNG/JPEG, HEIC/HEIF, MP4/MOV, texte, EML et PDF
|
||||
multipage, avec zoom de 25 à 400 %, ajustement, défilements horizontal et
|
||||
vertical, navigation et compteur de pages ; la provenance OCR reste alignée
|
||||
avec la page, le zoom et le défilement ;
|
||||
|
||||
Les dialogues métiers complexes suivent une politique GTK commune : parent
|
||||
réel via `transient_for`, modalité adaptée et présentation sans coordonnées
|
||||
absolues sous Wayland. Ils visent 1200 × 800, avec un minimum utile de
|
||||
800 × 600 lorsque la zone de travail le permet, un formulaire défilable à
|
||||
gauche sur environ deux tiers, un aperçu redimensionnable à droite et une
|
||||
barre d’actions fixe. Les alertes simples, popups `GtkDropDown` et sélecteurs
|
||||
de fichiers natifs ne sont pas concernés.
|
||||
|
||||
### Pivot EML
|
||||
|
||||
|
|
@ -390,6 +410,21 @@ Les nouveaux modules doivent être accompagnés de tests couvrant :
|
|||
- les responsabilités mémoire ;
|
||||
- les régressions possibles.
|
||||
|
||||
Les parcours GTK OCR et aperçu doivent aussi être exécutés séparément sur un
|
||||
poste avec affichage :
|
||||
|
||||
```bash
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_create_person_dialog_ocr_gtk
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_identity_import_gtk
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_workspace_identity_ocr_gtk
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_preview_widget_gtk
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_dialog_geometry_gtk
|
||||
```
|
||||
|
||||
Les tests emploient uniquement des documents `SPECIMEN`, des fichiers
|
||||
temporaires et des bases SQLite temporaires fermées puis rouvertes. Un `SKIP`
|
||||
lié à l’absence d’affichage ne remplace pas cette validation GTK réelle.
|
||||
|
||||
---
|
||||
|
||||
## Développement
|
||||
|
|
|
|||
|
|
@ -1,28 +1,36 @@
|
|||
# Architecture
|
||||
|
||||
`EvidencePreviewWidget` est l’adaptateur GTK unique de `EvidencePreview` pour
|
||||
`CreatePersonDialog` et la fiche directe. Il encapsule les états, le rendu
|
||||
EML/PDF/image/texte/vidéo, l’annulation, la génération, la garde de session et
|
||||
l’arrêt du média. Les deux écrans lui transmettent uniquement une source
|
||||
interne ou de staging accompagnée du SHA-256 attendu.
|
||||
`EvidencePreviewWidget` est l’adaptateur GTK partagé de `EvidencePreview` pour
|
||||
`CreatePersonDialog`, l’import, la révision OCR et la fiche directe. Il
|
||||
encapsule les états, le rendu EML/PDF/image/texte/vidéo, l’annulation, la
|
||||
génération, la garde de session et l’arrêt du média. Les écrans lui
|
||||
transmettent uniquement une source interne ou de staging accompagnée du
|
||||
SHA-256 attendu.
|
||||
|
||||
## Aperçu contrôlé multi-format
|
||||
|
||||
`EvidencePreview` vérifie l’empreinte du fichier interne ou de staging,
|
||||
détecte son contenu et produit un résultat borné sans GTK. `BackgroundTask`
|
||||
travaille hors du thread principal ; `CreatePersonDialog` y crée ensuite
|
||||
textures, buffers et médias, rejette les générations périmées et arrête tout
|
||||
travaille hors du thread principal ; le widget crée ensuite textures, buffers
|
||||
et médias sur le contexte GTK, rejette les générations périmées et arrête tout
|
||||
média au changement ou à la fermeture. L’aperçu ne persiste rien.
|
||||
|
||||
Pour les images et PDF, la barre compacte fournit zoom avant, zoom arrière et
|
||||
retour à l’ajustement. Une image agrandie reste défilable horizontalement et
|
||||
verticalement. Les PDF multipages disposent des actions page précédente et
|
||||
suivante et d’un compteur `Page X / N` ; le changement de page conserve le
|
||||
zoom. `OcrProvenanceOverlay` applique le même zoom, le même défilement et la
|
||||
même page afin que la provenance reste alignée.
|
||||
|
||||
Le mode aperçu de `EmlMimeExtractor` réutilise le parcours récursif et les
|
||||
décodages RFC 2047/2231 sans écrire les pièces jointes. Il préfère
|
||||
`text/plain`, transforme un éventuel HTML en texte inerte et retourne
|
||||
l’inventaire possédé. Le contrôleur vidéo indépendant de GTK orchestre
|
||||
pause, retour au début, détachement et libération via des actions injectées.
|
||||
|
||||
> **Version :** 3.1
|
||||
> **Dernière mise à jour :** 2026-07-28
|
||||
> **Schéma SQLite courant :** V14
|
||||
> **Version :** 3.2
|
||||
> **Dernière mise à jour :** 2026-07-30
|
||||
> **Schéma SQLite courant :** V17
|
||||
|
||||
## Personnes contextuelles — SQLite V14
|
||||
|
||||
|
|
@ -33,32 +41,64 @@ confiance et notes. Les anciens codes sont copiés littéralement depuis
|
|||
les rôles et le rattachement manuel dans une transaction unique.
|
||||
|
||||
Le cœur `EvidencePreview` vérifie l’intégrité avant décodage PNG/JPEG, applique
|
||||
des bornes et ne renvoie qu’un PNG mémoire réduit. Une `BackgroundTask`
|
||||
des bornes et ne renvoie qu’un rendu mémoire réduit. Une `BackgroundTask`
|
||||
travaille hors du thread GTK ; la texture est créée sur le contexte principal.
|
||||
Le dialogue annule l’ancienne tâche et rejette les générations obsolètes.
|
||||
L’interface conserve une génération de session et les chemins stables du
|
||||
projet et de sa base. PDF, vidéo, EML avancé et OCR restent exclus.
|
||||
projet et de sa base.
|
||||
|
||||
## OCR contrôlé d’identité — SQLite V15
|
||||
## OCR contrôlé d’identité — SQLite V15 à V17
|
||||
|
||||
La V15 sépare l’exécution OCR, l’observation du document et les observations
|
||||
de champs. Le moteur reçoit uniquement une copie contrôlée ou une page PDF
|
||||
explicitement choisie, conserve texte brut, TSV, paramètres, langues, version,
|
||||
SHA-256, confiance et coordonnées, puis laisse la révision à l’utilisateur.
|
||||
Les widgets n’accèdent pas à SQLite : le coordinateur persiste les décisions
|
||||
acceptées ou modifiées dans la transaction finale et compense les fichiers en
|
||||
cas d’échec.
|
||||
`IdentityOcrWorkflow` est l’unique orchestration du moteur OCR, réutilisée par
|
||||
`CreatePersonDialog`, `EvidenceIdentityOcrDialog`, l’import normal et
|
||||
Workspace. La V15 sépare l’exécution OCR, l’observation du document et les
|
||||
observations de champs. La V16 ajoute notamment l’origine `manual_entry` pour
|
||||
un champ visible mais omis par l’OCR. La V17 ajoute une transcription corrigée
|
||||
humaine distincte du texte OCR brut.
|
||||
|
||||
Le moteur reçoit uniquement une copie contrôlée ou une page PDF explicitement
|
||||
choisie, conserve texte brut, TSV, paramètres, langues, version, SHA-256,
|
||||
confiance et coordonnées, puis laisse la révision à l’utilisateur. Le texte
|
||||
brut n’est jamais remplacé. Une correction de valeur garde
|
||||
`manual_override`; une saisie sans valeur OCR garde `manual_entry`. Les notes
|
||||
restent factuelles et ne reconstruisent jamais une partie absente d’un
|
||||
document tronqué.
|
||||
|
||||
Les widgets n’accèdent pas à SQLite. Le coordinateur persiste sur l’UUID
|
||||
définitif de la preuve dans une transaction unique, avec rollback,
|
||||
compensation des fichiers et garde de session juste avant commit. L’import
|
||||
multiple reste sans OCR groupé : chaque preuve est analysée ensuite,
|
||||
individuellement et sans réimport ni doublon.
|
||||
|
||||
La provenance visuelle est rendue par `OcrProvenanceOverlay`. La conversion
|
||||
des coordonnées source vers la zone affichée est indépendante de GTK et tient
|
||||
compte du ratio, de la réduction, de l’agrandissement et des marges. Le
|
||||
rectangle est transitoire : aucune annotation n’est écrite dans la preuve.
|
||||
Les langues proposées proviennent exclusivement de `tesseract --list-langs`.
|
||||
Le DAO V15 expose des lectures possédées pour les exécutions, observations de
|
||||
documents et observations de champs, ainsi que des listes ordonnées par preuve,
|
||||
personne ou observation documentaire.
|
||||
Les DAO structurés exposent des lectures possédées pour les runs, observations
|
||||
de documents et de champs, notes et artefacts. La fiche de preuve recharge ces
|
||||
données depuis SQLite, sélectionne explicitement un run dans un modèle
|
||||
`GtkDropDown` stable et affiche texte brut, transcription corrigée, personne
|
||||
liée, SHA-256 et provenance graphique. Réviser conserve l’UUID du run et ne
|
||||
relance pas Tesseract ; relancer crée un nouveau run sans écraser
|
||||
l’historique.
|
||||
> **Statut :** architecture courante
|
||||
|
||||
## Politique des dialogues GTK métiers
|
||||
|
||||
`labfy_dialog_prepare()` et `labfy_dialog_present()` centralisent la séquence
|
||||
des dialogues complexes : `transient_for` vers la vraie fenêtre parente,
|
||||
modalité appropriée, géométrie commune, puis `gtk_window_present()`. Aucune
|
||||
coordonnée absolue n’est utilisée sous GTK4/Wayland.
|
||||
|
||||
La cible initiale est proche de 1200 × 800, avec un minimum utile de
|
||||
800 × 600 lorsque la zone de travail le permet. Un `GtkPaned` place
|
||||
initialement le formulaire défilable à gauche sur environ deux tiers et
|
||||
l’aperçu à droite sur un tiers ; sa position n’est appliquée qu’après la
|
||||
première allocation réelle puis reste entièrement modifiable. Les actions
|
||||
restent fixes en bas. Cette politique exclut les alertes simples, les popups
|
||||
`GtkDropDown` et les sélecteurs de fichiers natifs.
|
||||
|
||||
---
|
||||
|
||||
## 1. Objectif
|
||||
|
|
@ -355,7 +395,7 @@ validation du chemin
|
|||
↓
|
||||
création de l'arborescence
|
||||
↓
|
||||
initialisation transactionnelle de SQLite V14
|
||||
initialisation transactionnelle de SQLite V17
|
||||
↓
|
||||
création de l'identité de l'enquête
|
||||
↓
|
||||
|
|
@ -460,9 +500,9 @@ sont supprimées après rollback. `PersonCreationTask` exécute cette orchestrat
|
|||
hors du thread GTK. Un changement de session annule les tâches et interdit le
|
||||
rafraîchissement d’une autre enquête.
|
||||
|
||||
Les aperçus PNG/JPEG utilisent exclusivement le fichier interne d’une preuve
|
||||
existante ou sa copie de staging. PDF, vidéo, EML et autres formats ne sont
|
||||
pas analysés dans cette tranche.
|
||||
Les aperçus utilisent exclusivement le fichier interne d’une preuve existante
|
||||
ou sa copie de staging. L’OCR d’identité n’est lancé que sur demande explicite
|
||||
et sur un format compatible contrôlé.
|
||||
|
||||
Les tâches longues utilisent l'infrastructure de tâche d'arrière-plan et le
|
||||
gestionnaire de tâches.
|
||||
|
|
@ -791,6 +831,14 @@ Les scénarios critiques incluent :
|
|||
- conservation de la valeur brute ;
|
||||
- absence d'une dépendance optionnelle.
|
||||
|
||||
Les parcours OCR ferment puis rouvrent leurs bases SQLite temporaires afin de
|
||||
vérifier la persistance du texte brut, de la transcription corrigée, des
|
||||
champs et de l’historique multi-run. Les tests GTK réels passent par
|
||||
`MainWindow`, `Workspace` et les contrôles de production, avec des fixtures
|
||||
`SPECIMEN`, `G_DEBUG=fatal-criticals` et un timeout. Ils couvrent aussi
|
||||
l’aperçu PNG/JPEG/PDF multipage, la provenance, la géométrie, l’annulation,
|
||||
les résultats tardifs, les erreurs de transaction et les cycles de vie.
|
||||
|
||||
Validation :
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
# Guide de développement
|
||||
|
||||
Pour la tranche OCR identité, les tests ciblés sont
|
||||
`tests/test_identity_ocr`, `tests/test_identity_ocr_preprocessor`,
|
||||
Pour l’OCR d’identité, les tests ciblés sont `tests/test_identity_ocr`,
|
||||
`tests/test_identity_ocr_preprocessor`,
|
||||
`tests/test_person_creation_coordinator`,
|
||||
`tests/test_ocr_provenance_overlay_gtk` et
|
||||
`tests/test_create_person_dialog_gtk`. Le test Tesseract réel est facultatif
|
||||
`tests/test_create_person_dialog_ocr_gtk`,
|
||||
`tests/test_evidence_identity_import_gtk`,
|
||||
`tests/test_evidence_metadata_dialog_gtk`,
|
||||
`tests/test_workspace_identity_ocr_gtk` et
|
||||
`tests/test_ocr_provenance_overlay_gtk`. Le test Tesseract réel est facultatif
|
||||
et s’ignore explicitement si l’outil ou une langue compatible manque.
|
||||
Les fixtures directes du préprocesseur sont générées en mémoire ou dans un
|
||||
répertoire temporaire : JPEG avec APP1 EXIF, HEIC/HEIF via libheif et PDF
|
||||
multipage via Cairo. Elles ne doivent jamais être remplacées par un document
|
||||
réel.
|
||||
|
||||
La validation ciblée du composant partagé comprend
|
||||
La validation ciblée de l’aperçu partagé comprend
|
||||
`test_evidence_preview_widget_gtk`, `test_evidence_preview`,
|
||||
`test_evidence_video_preview_controller` et
|
||||
`test_create_person_dialog_gtk`, ce dernier sous
|
||||
|
|
@ -27,8 +30,8 @@ conteneur synthétique. Les transformations d’orientation libheif restent
|
|||
appliquées par le décodage standard ; seule l’image principale est rendue et
|
||||
le nombre d’images de premier niveau est exposé.
|
||||
|
||||
> **Version :** 2.1
|
||||
> **Dernière mise à jour :** 2026-07-28
|
||||
> **Version :** 2.2
|
||||
> **Dernière mise à jour :** 2026-07-30
|
||||
> **Projet :** Labfy Investigation
|
||||
|
||||
---
|
||||
|
|
@ -282,6 +285,30 @@ make -j8 \
|
|||
Ces tests utilisent uniquement des fichiers `SPECIMEN`, des répertoires
|
||||
temporaires et des bases SQLite temporaires.
|
||||
|
||||
Tests GTK réels de l’OCR, de l’aperçu et de la géométrie :
|
||||
|
||||
```sh
|
||||
make -j8 \
|
||||
tests/test_create_person_dialog_ocr_gtk \
|
||||
tests/test_evidence_identity_import_gtk \
|
||||
tests/test_evidence_metadata_dialog_gtk \
|
||||
tests/test_workspace_identity_ocr_gtk \
|
||||
tests/test_evidence_preview_widget_gtk \
|
||||
tests/test_dialog_geometry_gtk
|
||||
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_create_person_dialog_ocr_gtk
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_identity_import_gtk
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_metadata_dialog_gtk
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_workspace_identity_ocr_gtk
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_preview_widget_gtk
|
||||
G_DEBUG=fatal-criticals timeout 30s ./tests/test_dialog_geometry_gtk
|
||||
```
|
||||
|
||||
Ils doivent cliquer sur les contrôles de production, passer par `MainWindow`
|
||||
et `Workspace` lorsque le scénario le demande, puis fermer et rouvrir la base
|
||||
SQLite temporaire. Un environnement sans affichage peut produire un `SKIP`,
|
||||
mais celui-ci ne remplace pas l’exécution séparée sur le poste GTK réel.
|
||||
|
||||
La fixture manuelle est
|
||||
`tests/fixtures/eml/manual_smoke_test.eml`. Elle est exclusivement
|
||||
synthétique. Pour la validation GTK, créer une enquête neuve dans un
|
||||
|
|
@ -541,6 +568,15 @@ Un dialogue :
|
|||
- ne réalise pas une opération longue directement ;
|
||||
- présente un résumé avant une écriture importante.
|
||||
|
||||
Les dialogues métiers complexes utilisent `labfy_dialog_prepare()` puis
|
||||
`labfy_dialog_present()`. La vraie fenêtre parente est définie avec
|
||||
`transient_for`; aucune coordonnée absolue n’est utilisée. Ils visent
|
||||
1200 × 800 et un minimum utile de 800 × 600, avec formulaire défilable à
|
||||
gauche sur environ deux tiers, aperçu à droite et barre d’actions fixe. Le
|
||||
séparateur reste déplaçable et n’est pas réinitialisé après sa première
|
||||
allocation. Cette règle ne concerne pas les alertes, popups `GtkDropDown` ou
|
||||
sélecteurs natifs.
|
||||
|
||||
### 11.3 Messages utilisateur
|
||||
|
||||
Les détails techniques restent dans les journaux.
|
||||
|
|
@ -663,3 +699,24 @@ Avant de considérer une modification terminée :
|
|||
- [ ] `make -j8 test` passe ;
|
||||
- [ ] `git diff --check` passe ;
|
||||
- [ ] la documentation est à jour.
|
||||
|
||||
## 16. Validation manuelle du ticket #109
|
||||
|
||||
Utiliser exclusivement une enquête, une base SQLite et des documents
|
||||
`SPECIMEN` temporaires. Vérifier au minimum :
|
||||
|
||||
1. création d’une personne avec OCR, correction de transcription, réédition
|
||||
d’un champ, `manual_entry`, notes et confirmation ;
|
||||
2. visibilité immédiate de la preuve dans Workspace, puis fermeture et
|
||||
réouverture de SQLite ;
|
||||
3. import multiple sans OCR, puis analyse individuelle de chaque preuve depuis
|
||||
sa fiche, sans doublon ;
|
||||
4. sélection de plusieurs `OcrRun`, révision du seul run choisi sans
|
||||
Tesseract, puis nouvelle analyse créant un run supplémentaire ;
|
||||
5. aperçu PNG, JPEG et PDF `SPECIMEN` multipage : zoom, ajustement,
|
||||
défilements, navigation, compteur et provenance ;
|
||||
6. dialogues à 1200 × 800 et 760 × 560 : parent transitoire, formulaire
|
||||
défilable, répartition 2/3–1/3 et actions toujours visibles.
|
||||
|
||||
La validation manuelle réussie confirme ces parcours, mais ne clôt pas le
|
||||
ticket #109 et ne couvre pas les limitations encore listées dans la roadmap.
|
||||
|
|
|
|||
|
|
@ -1,34 +1,23 @@
|
|||
# Roadmap
|
||||
|
||||
La tranche 3 du ticket #109 ajoute l’OCR contrôlé des documents d’identité dans
|
||||
l’assistant de création : lancement explicite, staging, révision champ par
|
||||
champ et persistance transactionnelle V15. Elle ne clôt pas le ticket #109 et
|
||||
n’engage aucune tranche suivante.
|
||||
La provenance graphique, les langues Tesseract dynamiques et la compensation
|
||||
complète des répertoires OCR font partie de cette même tranche.
|
||||
La lecture structurée V15 et les scénarios de fermeture/réouverture restent
|
||||
également circonscrits à la tranche 3.
|
||||
L’OCR contrôlé est limité à une preuve par opération dans l’import normal.
|
||||
L’import multiple sans OCR reste disponible. L’OCR multiple constitue un
|
||||
chantier futur distinct ; les preuves sont actuellement analysées
|
||||
successivement afin de conserver une association non ambiguë.
|
||||
Le ticket #109 reste en cours. Ses tranches livrées couvrent l’OCR contrôlé et
|
||||
révisable dans la création d’une personne, l’import normal et la fiche de
|
||||
preuve, la transcription corrigée persistante, l’historique multi-run, la
|
||||
provenance graphique, ainsi que l’aperçu avancé partagé. L’import multiple
|
||||
reste sans OCR groupé : chaque preuve est analysée individuellement après
|
||||
import afin de préserver une association non ambiguë.
|
||||
|
||||
La tranche 2 partage désormais l’aperçu entre l’assistant personne et la
|
||||
fiche directe. La validation manuelle des codecs installés reste nécessaire ;
|
||||
elle ne vaut pas achèvement du ticket #109 et n’inclut aucun OCR.
|
||||
L’aperçu couvre PNG/JPEG, HEIC/HEIF, MP4/MOV, texte, EML passif et PDF
|
||||
multipage, avec zoom, ajustement, défilements, navigation et compteur. La
|
||||
politique commune des dialogues métiers fournit une géométrie responsive, un
|
||||
formulaire défilable, un aperçu redimensionnable et des actions fixes.
|
||||
|
||||
L’aperçu contrôlé du ticket #109 couvre PNG/JPEG, HEIC/HEIF, MP4/MOV, texte,
|
||||
EML passif et première page PDF. OCR et analyse avancée restent hors périmètre.
|
||||
La validation manuelle de ces parcours est réussie. Elle ne vaut pas
|
||||
achèvement du ticket #109.
|
||||
|
||||
> **Dernière mise à jour :** 2026-07-28
|
||||
> **Dernière mise à jour :** 2026-07-30
|
||||
> **État du projet :** développement actif
|
||||
> **Schéma SQLite courant :** V14
|
||||
|
||||
Les tranches 1 et 2 du ticket #109 couvrent les rôles contextuels multiples,
|
||||
la sélection de plusieurs preuves existantes, le staging et l’import multiple
|
||||
différé, ainsi que la création transactionnelle avec compensation des
|
||||
fichiers. OCR, extraction de documents d’identité et analyses documentaires
|
||||
avancées restent hors périmètre.
|
||||
> **Schéma SQLite courant :** V17
|
||||
> **Usage opérationnel :** non prêt pour la production
|
||||
|
||||
---
|
||||
|
|
@ -142,7 +131,7 @@ La branche `main` contient notamment :
|
|||
|
||||
- création, validation et ouverture d'enquêtes ;
|
||||
- session d'enquête remplaçable proprement ;
|
||||
- infrastructure SQLite et migrations jusqu'à V14 ;
|
||||
- infrastructure SQLite et migrations jusqu'à V17 ;
|
||||
- couche Database, DAO et services métier ;
|
||||
- import de preuves avec copie contrôlée et SHA-256 ;
|
||||
- vérification d'intégrité et reclassement des preuves ;
|
||||
|
|
@ -170,6 +159,40 @@ Cette liste est une synthèse et non un contrat de stabilité.
|
|||
|
||||
## 5. Chantier actif
|
||||
|
||||
### Ticket #109 — Personnes, preuves et OCR d’identité
|
||||
|
||||
Statut technique :
|
||||
|
||||
```text
|
||||
PARTIEL — VALIDATION MANUELLE DES TRANCHES LIVRÉES RÉUSSIE
|
||||
```
|
||||
|
||||
Éléments livrés :
|
||||
|
||||
- sélection de preuves existantes, staging et import multiple différé ;
|
||||
- OCR contrôlé, correction rééditable et transcription corrigée distincte ;
|
||||
- persistance transactionnelle sur l’UUID définitif avec historique multi-run ;
|
||||
- consultation, révision sans Tesseract et nouvelle analyse depuis la fiche ;
|
||||
- provenance graphique et aperçu partagé avec PDF multipage ;
|
||||
- politique commune de géométrie des dialogues GTK métiers.
|
||||
|
||||
Limitations restantes :
|
||||
|
||||
1. l’OCR groupé de plusieurs preuves dans une même opération n’est pas pris en
|
||||
charge ;
|
||||
2. le statut contrôlé d’authenticité du document et la justification
|
||||
obligatoire des statuts affirmatifs ne sont pas implémentés ;
|
||||
3. la relation factuelle typée entre personne et preuve et certains états
|
||||
d’identification demandés restent incomplets ;
|
||||
4. une partie du vocabulaire des rôles demeure codée en dur ;
|
||||
5. la provenance uniforme de tous les dérivés et la projection des seules
|
||||
valeurs OCR confirmées vers les attributs structurés de la personne restent
|
||||
partielles ;
|
||||
6. les tests négatifs garantissant l’absence de relation automatique
|
||||
attribuant l’identité ou le rôle d’auteur doivent encore être renforcés.
|
||||
|
||||
Ces limites interdisent de présenter le ticket #109 comme terminé.
|
||||
|
||||
### Ticket #107 — Pivot e-mail forensique
|
||||
|
||||
Statut technique :
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ typedef struct {
|
|||
char *expected_sha256;
|
||||
char *mime_type;
|
||||
guint64 request_generation;
|
||||
guint pdf_page;
|
||||
} EvidencePreviewRequest;
|
||||
typedef struct {
|
||||
EvidencePreviewKind kind;
|
||||
|
|
@ -40,6 +41,7 @@ typedef struct {
|
|||
char *message;
|
||||
guint64 size_bytes;
|
||||
guint item_count;
|
||||
guint current_page;
|
||||
gboolean integrity_valid;
|
||||
gboolean preview_available;
|
||||
gboolean truncated;
|
||||
|
|
@ -47,6 +49,8 @@ typedef struct {
|
|||
EvidencePreviewRequest *evidence_preview_request_new(const char *root,
|
||||
const char *evidence_identifier, const char *relative_path,
|
||||
const char *sha256, const char *mime_type, guint64 generation);
|
||||
void evidence_preview_request_set_pdf_page(
|
||||
EvidencePreviewRequest *request, guint page);
|
||||
void evidence_preview_request_free(EvidencePreviewRequest *request);
|
||||
EvidencePreviewResult *evidence_preview_load(
|
||||
const EvidencePreviewRequest *request, GCancellable *cancellable,
|
||||
|
|
|
|||
30
include/views/dialog_geometry.h
Normal file
30
include/views/dialog_geometry.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#ifndef LABFY_DIALOG_GEOMETRY_H
|
||||
#define LABFY_DIALOG_GEOMETRY_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
typedef struct {
|
||||
int initial_width;
|
||||
int initial_height;
|
||||
int minimum_width;
|
||||
int minimum_height;
|
||||
} LabfyDialogGeometry;
|
||||
|
||||
LabfyDialogGeometry labfy_dialog_geometry_calculate(
|
||||
int workarea_width, int workarea_height);
|
||||
|
||||
void labfy_dialog_apply_standard_geometry(
|
||||
GtkWindow *window, GtkWindow *parent);
|
||||
|
||||
void labfy_dialog_prepare(
|
||||
GtkWindow *window,
|
||||
GtkWindow *parent,
|
||||
gboolean modal,
|
||||
gboolean destroy_with_parent);
|
||||
|
||||
void labfy_dialog_present(GtkWindow *window);
|
||||
|
||||
void labfy_paned_apply_initial_ratio(
|
||||
GtkPaned *paned, double ratio, int minimum_start, int minimum_end);
|
||||
|
||||
#endif
|
||||
|
|
@ -25,6 +25,19 @@ const char *evidence_preview_widget_get_state(
|
|||
const EvidencePreviewWidget *widget);
|
||||
char *evidence_preview_widget_dup_text(
|
||||
const EvidencePreviewWidget *widget);
|
||||
void evidence_preview_widget_zoom_in(EvidencePreviewWidget *widget);
|
||||
void evidence_preview_widget_zoom_out(EvidencePreviewWidget *widget);
|
||||
void evidence_preview_widget_fit(EvidencePreviewWidget *widget);
|
||||
double evidence_preview_widget_get_zoom(
|
||||
const EvidencePreviewWidget *widget);
|
||||
gboolean evidence_preview_widget_is_fit(
|
||||
const EvidencePreviewWidget *widget);
|
||||
gboolean evidence_preview_widget_set_pdf_page(
|
||||
EvidencePreviewWidget *widget, guint page);
|
||||
guint evidence_preview_widget_get_pdf_page(
|
||||
const EvidencePreviewWidget *widget);
|
||||
guint evidence_preview_widget_get_pdf_page_count(
|
||||
const EvidencePreviewWidget *widget);
|
||||
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ void ocr_provenance_overlay_set_field(OcrProvenanceOverlay *overlay,
|
|||
const IdentityFieldObservation *field, guint64 generation);
|
||||
void ocr_provenance_overlay_clear(OcrProvenanceOverlay *overlay,
|
||||
guint64 generation);
|
||||
void ocr_provenance_overlay_set_page(
|
||||
OcrProvenanceOverlay *overlay, guint page, guint64 generation);
|
||||
void ocr_provenance_overlay_zoom_in(OcrProvenanceOverlay *overlay);
|
||||
void ocr_provenance_overlay_zoom_out(OcrProvenanceOverlay *overlay);
|
||||
void ocr_provenance_overlay_fit(OcrProvenanceOverlay *overlay);
|
||||
double ocr_provenance_overlay_get_zoom(
|
||||
const OcrProvenanceOverlay *overlay);
|
||||
gboolean ocr_provenance_overlay_has_region(
|
||||
const OcrProvenanceOverlay *overlay);
|
||||
|
||||
|
|
|
|||
|
|
@ -299,8 +299,8 @@ failed:
|
|||
if (handle != NULL) heif_image_handle_release(handle);
|
||||
heif_context_free(context); return FALSE;
|
||||
}
|
||||
static gboolean load_pdf(const char *path, EvidencePreviewResult *result,
|
||||
GError **error)
|
||||
static gboolean load_pdf(const char *path, guint requested_page,
|
||||
EvidencePreviewResult *result, GError **error)
|
||||
{
|
||||
char *uri = g_filename_to_uri(path, NULL, error);
|
||||
if (uri == NULL) return FALSE;
|
||||
|
|
@ -309,7 +309,21 @@ static gboolean load_pdf(const char *path, EvidencePreviewResult *result,
|
|||
gint pages = poppler_document_get_n_pages(document);
|
||||
if (pages < 1) { g_object_unref(document); g_set_error_literal(error,
|
||||
preview_error(), 7, "Le PDF ne contient aucune page."); return FALSE; }
|
||||
PopplerPage *page = poppler_document_get_page(document, 0);
|
||||
if (requested_page >= (guint) pages) {
|
||||
g_object_unref(document);
|
||||
g_set_error(error, preview_error(), 8,
|
||||
"La page PDF %u est hors limites (document de %d pages).",
|
||||
requested_page + 1U, pages);
|
||||
return FALSE;
|
||||
}
|
||||
PopplerPage *page = poppler_document_get_page(
|
||||
document, (gint) requested_page);
|
||||
if (page == NULL) {
|
||||
g_object_unref(document);
|
||||
g_set_error_literal(error, preview_error(), 7,
|
||||
"La page PDF demandée est indisponible.");
|
||||
return FALSE;
|
||||
}
|
||||
double page_width = 0, page_height = 0;
|
||||
poppler_page_get_size(page, &page_width, &page_height);
|
||||
if (!valid_dimensions((gint) page_width, (gint) page_height, error)) {
|
||||
|
|
@ -324,6 +338,7 @@ static gboolean load_pdf(const char *path, EvidencePreviewResult *result,
|
|||
cairo_t *cr = cairo_create(surface); cairo_set_source_rgb(cr, 1, 1, 1);
|
||||
cairo_paint(cr); cairo_scale(cr, scale, scale); poppler_page_render(page, cr);
|
||||
cairo_destroy(cr); result->item_count = (guint) pages;
|
||||
result->current_page = requested_page + 1U;
|
||||
gboolean ok = surface_to_result(surface, width, height, result, error);
|
||||
cairo_surface_destroy(surface); g_object_unref(page);
|
||||
g_object_unref(document); return ok;
|
||||
|
|
@ -396,6 +411,12 @@ EvidencePreviewRequest *evidence_preview_request_new(const char *root,
|
|||
request->mime_type = g_strdup(mime_type);
|
||||
request->request_generation = generation; return request;
|
||||
}
|
||||
|
||||
void evidence_preview_request_set_pdf_page(
|
||||
EvidencePreviewRequest *request, guint page)
|
||||
{
|
||||
if (request != NULL) request->pdf_page = page;
|
||||
}
|
||||
void evidence_preview_request_free(EvidencePreviewRequest *request)
|
||||
{
|
||||
if (request == NULL) return;
|
||||
|
|
@ -501,7 +522,7 @@ EvidencePreviewResult *evidence_preview_load(
|
|||
result->kind = EVIDENCE_PREVIEW_KIND_PDF;
|
||||
result->effective_mime_type = g_strdup("application/pdf");
|
||||
result->effective_format = g_strdup("PDF");
|
||||
ok = load_pdf(path, result, error); break;
|
||||
ok = load_pdf(path, request->pdf_page, result, error); break;
|
||||
default:
|
||||
result->message = g_strdup("Format non pris en charge ou contenu invalide.");
|
||||
result->effective_format = g_strdup("Inconnu"); break;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
static EvidencePreviewRequest *copy_request(const EvidencePreviewRequest *r)
|
||||
{
|
||||
return evidence_preview_request_new(r->investigation_root_path,
|
||||
EvidencePreviewRequest *copy = evidence_preview_request_new(
|
||||
r->investigation_root_path,
|
||||
r->evidence_identifier, r->relative_path, r->expected_sha256,
|
||||
r->mime_type, r->request_generation);
|
||||
evidence_preview_request_set_pdf_page(copy, r->pdf_page);
|
||||
return copy;
|
||||
}
|
||||
static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
|
||||
gpointer data, gpointer *result, GError **error)
|
||||
|
|
|
|||
|
|
@ -4,22 +4,81 @@
|
|||
#include "core/identity_field_extractor.h"
|
||||
#include "core/ocr_analysis.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static gboolean identity_ocr_workflow_languages_are_valid(
|
||||
const char *languages)
|
||||
{
|
||||
char **codes;
|
||||
gboolean valid = TRUE;
|
||||
|
||||
if (languages == NULL || languages[0] == '\0')
|
||||
return FALSE;
|
||||
codes = g_strsplit(languages, "+", -1);
|
||||
for (guint index = 0; codes[index] != NULL && valid; index++) {
|
||||
const char *code = codes[index];
|
||||
|
||||
valid = strlen(code) == 3;
|
||||
for (guint character = 0; valid && code[character] != '\0';
|
||||
character++)
|
||||
valid = g_ascii_islower(code[character]);
|
||||
}
|
||||
g_strfreev(codes);
|
||||
return valid;
|
||||
}
|
||||
|
||||
static gboolean identity_ocr_workflow_validate_request(
|
||||
const IdentityOcrWorkflowRequest *request,
|
||||
GError **error)
|
||||
{
|
||||
#define REQUIRE_REQUEST_FIELD(condition, message) \
|
||||
G_STMT_START { \
|
||||
if (!(condition)) { \
|
||||
g_set_error_literal(error, G_IO_ERROR, \
|
||||
G_IO_ERROR_INVALID_ARGUMENT, (message)); \
|
||||
return FALSE; \
|
||||
} \
|
||||
} G_STMT_END
|
||||
|
||||
REQUIRE_REQUEST_FIELD(request != NULL,
|
||||
"La demande OCR d’identité est absente.");
|
||||
REQUIRE_REQUEST_FIELD(request->root_path != NULL,
|
||||
"Le chemin racine de l’enquête est absent de la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(request->evidence_identifier != NULL,
|
||||
"L’UUID de la preuve est absent de la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(request->relative_path != NULL,
|
||||
"Le chemin relatif de la preuve est absent de la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(request->expected_sha256 != NULL,
|
||||
"Le SHA-256 attendu est absent de la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(request->mime_type != NULL,
|
||||
"Le type MIME de la preuve est absent de la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(
|
||||
identity_ocr_document_type_is_valid(request->document_type),
|
||||
"Le type de document d’identité est invalide dans la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(
|
||||
identity_ocr_document_side_is_valid(request->document_side),
|
||||
"La face du document d’identité est invalide dans la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(
|
||||
identity_ocr_workflow_languages_are_valid(request->languages),
|
||||
"Le code de langue Tesseract est invalide dans la demande OCR "
|
||||
"(un code ISO 639-3 comme « fra » ou « eng » est requis).");
|
||||
REQUIRE_REQUEST_FIELD(request->preprocessing_profile != NULL,
|
||||
"Le profil de prétraitement est absent de la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(request->tesseract_executable != NULL,
|
||||
"Le chemin de l’exécutable Tesseract est absent de la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(request->page_number > 0,
|
||||
"Le numéro de page est invalide dans la demande OCR.");
|
||||
REQUIRE_REQUEST_FIELD(request->profile <= IDENTITY_OCR_PREPROCESS_UPSCALE,
|
||||
"Le profil de prétraitement est invalide dans la demande OCR.");
|
||||
|
||||
#undef REQUIRE_REQUEST_FIELD
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean identity_ocr_workflow_request_is_valid(
|
||||
const IdentityOcrWorkflowRequest *request)
|
||||
{
|
||||
return request != NULL &&
|
||||
request->root_path != NULL &&
|
||||
request->evidence_identifier != NULL &&
|
||||
request->relative_path != NULL &&
|
||||
request->expected_sha256 != NULL &&
|
||||
request->mime_type != NULL &&
|
||||
identity_ocr_document_type_is_valid(request->document_type) &&
|
||||
identity_ocr_document_side_is_valid(request->document_side) &&
|
||||
request->languages != NULL &&
|
||||
request->preprocessing_profile != NULL &&
|
||||
request->tesseract_executable != NULL &&
|
||||
request->page_number > 0 &&
|
||||
request->profile <= IDENTITY_OCR_PREPROCESS_UPSCALE;
|
||||
return identity_ocr_workflow_validate_request(request, NULL);
|
||||
}
|
||||
|
||||
IdentityOcrRun *identity_ocr_workflow_execute(
|
||||
|
|
@ -37,11 +96,8 @@ IdentityOcrRun *identity_ocr_workflow_execute(
|
|||
gsize preview_length = 0;
|
||||
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
||||
if (!identity_ocr_workflow_request_is_valid(request)) {
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"La demande du parcours OCR d’identité est invalide.");
|
||||
if (!identity_ocr_workflow_validate_request(request, error))
|
||||
return NULL;
|
||||
}
|
||||
preview_request = evidence_preview_request_new(request->root_path,
|
||||
request->evidence_identifier, request->relative_path,
|
||||
request->expected_sha256, request->mime_type, request->generation);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* @brief Formulaire GTK de création d'une personne observée.
|
||||
******************************************************************************/
|
||||
#include "views/create_person_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
#include "core/evidence_staging.h"
|
||||
#include "core/evidence_staging_task.h"
|
||||
#include "core/person_confirmation_summary.h"
|
||||
|
|
@ -49,6 +50,7 @@ typedef struct
|
|||
EvidencePreviewWidget *preview;
|
||||
GtkLabel *progress;
|
||||
GtkLabel *summary;
|
||||
GtkScrolledWindow *summary_scroll;
|
||||
GtkStack *stack;
|
||||
GtkButton *previous;
|
||||
GtkButton *next;
|
||||
|
|
@ -132,25 +134,6 @@ static void create_person_dialog_on_retained_changed(
|
|||
static void create_person_dialog_render_ocr_fields(
|
||||
CreatePersonDialogState *state, IdentityOcrRun *run);
|
||||
|
||||
static gboolean create_person_dialog_set_initial_ocr_paned_position(
|
||||
gpointer data)
|
||||
{
|
||||
GtkPaned *paned = GTK_PANED(data);
|
||||
int width;
|
||||
int position;
|
||||
if (g_object_get_data(
|
||||
G_OBJECT(paned), "create-person-ocr-position-set") != NULL)
|
||||
return G_SOURCE_REMOVE;
|
||||
width = gtk_widget_get_width(GTK_WIDGET(paned));
|
||||
if (width <= 0) return G_SOURCE_CONTINUE;
|
||||
position = MAX(500, (width * 2) / 3);
|
||||
position = MIN(position, MAX(0, width - 180));
|
||||
gtk_paned_set_position(paned, position);
|
||||
g_object_set_data(G_OBJECT(paned),
|
||||
"create-person-ocr-position-set", GINT_TO_POINTER(1));
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void identity_language_job_free(gpointer data)
|
||||
{
|
||||
IdentityLanguageJob *job = data;
|
||||
|
|
@ -187,6 +170,53 @@ static const char *identity_language_label(const char *code)
|
|||
return code;
|
||||
}
|
||||
|
||||
static guint identity_default_language_index(const GPtrArray *codes)
|
||||
{
|
||||
guint english_index = GTK_INVALID_LIST_POSITION;
|
||||
|
||||
for (guint index = 0; codes != NULL && index < codes->len; index++) {
|
||||
const char *code = g_ptr_array_index((GPtrArray *) codes, index);
|
||||
|
||||
if (g_strcmp0(code, "fra") == 0)
|
||||
return index;
|
||||
if (g_strcmp0(code, "eng") == 0)
|
||||
english_index = index;
|
||||
}
|
||||
return english_index != GTK_INVALID_LIST_POSITION
|
||||
? english_index
|
||||
: (codes != NULL && codes->len > 0
|
||||
? 0
|
||||
: GTK_INVALID_LIST_POSITION);
|
||||
}
|
||||
|
||||
static const char *create_person_dialog_effective_ocr_mime(
|
||||
const PersonEvidenceSelectionItem *item)
|
||||
{
|
||||
const char *mime =
|
||||
person_evidence_selection_item_get_mime_type(item);
|
||||
const char *name;
|
||||
const char *suffix;
|
||||
|
||||
if (mime != NULL && mime[0] != '\0')
|
||||
return mime;
|
||||
name = person_evidence_selection_item_get_original_name(item);
|
||||
suffix = name != NULL ? strrchr(name, '.') : NULL;
|
||||
if (suffix == NULL)
|
||||
return NULL;
|
||||
if (g_ascii_strcasecmp(suffix, ".png") == 0)
|
||||
return "image/png";
|
||||
if (g_ascii_strcasecmp(suffix, ".jpg") == 0 ||
|
||||
g_ascii_strcasecmp(suffix, ".jpeg") == 0)
|
||||
return "image/jpeg";
|
||||
if (g_ascii_strcasecmp(suffix, ".heic") == 0)
|
||||
return "image/heic";
|
||||
if (g_ascii_strcasecmp(suffix, ".heif") == 0)
|
||||
return "image/heif";
|
||||
if (g_ascii_strcasecmp(suffix, ".pdf") == 0)
|
||||
return "application/pdf";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void identity_languages_completed(GObject *source,
|
||||
GAsyncResult *result, gpointer data)
|
||||
{
|
||||
|
|
@ -209,9 +239,7 @@ static void identity_languages_completed(GObject *source,
|
|||
}
|
||||
gtk_drop_down_set_model(state->ocr_languages, G_LIST_MODEL(labels));
|
||||
gtk_drop_down_set_selected(state->ocr_languages,
|
||||
state->ocr_language_codes != NULL &&
|
||||
state->ocr_language_codes->len > 0 ? 0 :
|
||||
GTK_INVALID_LIST_POSITION);
|
||||
identity_default_language_index(state->ocr_language_codes));
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(state->ocr_start),
|
||||
state->ocr_language_codes != NULL &&
|
||||
state->ocr_language_codes->len > 0);
|
||||
|
|
@ -598,6 +626,8 @@ static void create_person_dialog_ocr_completed(GObject *source,
|
|||
create_person_dialog_render_ocr_fields(state, run);
|
||||
ocr_provenance_overlay_set_image(state->ocr_overlay,
|
||||
identity_ocr_run_get_preview(run), state->ocr_generation);
|
||||
ocr_provenance_overlay_set_page(state->ocr_overlay,
|
||||
identity_ocr_run_get_page(run), state->ocr_generation);
|
||||
const GPtrArray *fields = identity_ocr_run_get_fields(run);
|
||||
if (fields != NULL && fields->len > 0)
|
||||
ocr_provenance_overlay_set_field(state->ocr_overlay,
|
||||
|
|
@ -653,7 +683,7 @@ static void create_person_dialog_on_ocr_start(GtkButton *button,
|
|||
job->identifier = g_strdup(
|
||||
person_evidence_selection_item_get_identifier(item));
|
||||
job->sha256 = g_strdup(person_evidence_selection_item_get_sha256(item));
|
||||
job->mime = g_strdup(person_evidence_selection_item_get_mime_type(item));
|
||||
job->mime = g_strdup(create_person_dialog_effective_ocr_mime(item));
|
||||
if (person_evidence_selection_item_get_origin(item) ==
|
||||
PERSON_EVIDENCE_ORIGIN_EXISTING) {
|
||||
record = person_evidence_selection_item_get_record(item);
|
||||
|
|
@ -1430,6 +1460,10 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
|
|||
}
|
||||
}
|
||||
gtk_label_set_text(state->summary, confirmation->str);
|
||||
GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(
|
||||
state->summary_scroll);
|
||||
gtk_adjustment_set_value(adjustment,
|
||||
gtk_adjustment_get_lower(adjustment));
|
||||
g_string_free(confirmation, TRUE);
|
||||
g_free(text);
|
||||
g_free(notes);
|
||||
|
|
@ -1524,9 +1558,7 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
gtk_window_set_application(state->window,
|
||||
gtk_window_get_application(parent));
|
||||
gtk_window_set_title(state->window, "Ajouter une personne");
|
||||
gtk_window_set_transient_for(state->window, parent);
|
||||
gtk_window_set_modal(state->window, TRUE);
|
||||
gtk_window_set_default_size(state->window, 960, 700);
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(box, 16); gtk_widget_set_margin_end(box, 16);
|
||||
gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16);
|
||||
|
|
@ -1554,6 +1586,8 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
}
|
||||
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(
|
||||
G_LIST_MODEL(g_object_ref(state->evidence_labels)), NULL));
|
||||
gtk_widget_set_name(GTK_WIDGET(state->evidence),
|
||||
"create-person-evidence-dropdown");
|
||||
state->search = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(state->search,
|
||||
"Rechercher par nom, description ou type");
|
||||
|
|
@ -1565,6 +1599,8 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
state->type_filter = GTK_DROP_DOWN(
|
||||
gtk_drop_down_new(G_LIST_MODEL(
|
||||
g_object_ref(state->type_filter_labels)), NULL));
|
||||
gtk_widget_set_name(GTK_WIDGET(state->type_filter),
|
||||
"create-person-evidence-type-filter");
|
||||
/*
|
||||
* gtk_drop_down_new() prend la propriété complète du modèle. Une
|
||||
* libération ici laisserait les vues internes du GtkDropDown avec un
|
||||
|
|
@ -1655,10 +1691,20 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_widget_set_name(evidence_paned, "create-person-evidence-paned");
|
||||
gtk_widget_set_size_request(evidence_box, 320, -1);
|
||||
gtk_paned_set_start_child(GTK_PANED(evidence_paned), evidence_box);
|
||||
GtkWidget *evidence_scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_name(evidence_scroll,
|
||||
"create-person-evidence-scroll");
|
||||
gtk_scrolled_window_set_policy(
|
||||
GTK_SCROLLED_WINDOW(evidence_scroll),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_child(
|
||||
GTK_SCROLLED_WINDOW(evidence_scroll), evidence_box);
|
||||
gtk_paned_set_start_child(
|
||||
GTK_PANED(evidence_paned), evidence_scroll);
|
||||
gtk_paned_set_end_child(GTK_PANED(evidence_paned),
|
||||
evidence_preview_widget_get_widget(state->preview));
|
||||
gtk_paned_set_position(GTK_PANED(evidence_paned), 420);
|
||||
labfy_paned_apply_initial_ratio(
|
||||
GTK_PANED(evidence_paned), 2.0 / 3.0, 480, 240);
|
||||
gtk_paned_set_resize_start_child(
|
||||
GTK_PANED(evidence_paned), FALSE);
|
||||
gtk_paned_set_resize_end_child(GTK_PANED(evidence_paned), TRUE);
|
||||
|
|
@ -1689,6 +1735,8 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
"Détection des langues…", NULL};
|
||||
state->ocr_languages = GTK_DROP_DOWN(
|
||||
gtk_drop_down_new_from_strings(pending_languages));
|
||||
gtk_widget_set_name(GTK_WIDGET(state->ocr_languages),
|
||||
"create-person-ocr-languages");
|
||||
}
|
||||
state->ocr_profile = GTK_DROP_DOWN(
|
||||
gtk_drop_down_new_from_strings(profile_labels));
|
||||
|
|
@ -1835,9 +1883,23 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
state->summary = GTK_LABEL(gtk_label_new(""));
|
||||
summary = GTK_WIDGET(state->summary);
|
||||
gtk_label_set_wrap(state->summary, TRUE);
|
||||
gtk_label_set_wrap_mode(state->summary, PANGO_WRAP_WORD_CHAR);
|
||||
gtk_label_set_max_width_chars(state->summary, 100);
|
||||
gtk_label_set_xalign(state->summary, 0.0f);
|
||||
gtk_label_set_selectable(state->summary, TRUE);
|
||||
gtk_stack_add_titled(state->stack, summary, "summary", "5 — Confirmation");
|
||||
gtk_widget_set_hexpand(summary, TRUE);
|
||||
gtk_widget_set_valign(summary, GTK_ALIGN_START);
|
||||
state->summary_scroll = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
|
||||
gtk_widget_set_name(GTK_WIDGET(state->summary_scroll),
|
||||
"create-person-confirmation-scroll");
|
||||
gtk_scrolled_window_set_policy(state->summary_scroll,
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_propagate_natural_height(
|
||||
state->summary_scroll, FALSE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(state->summary_scroll), TRUE);
|
||||
gtk_scrolled_window_set_child(state->summary_scroll, summary);
|
||||
gtk_stack_add_titled(state->stack,
|
||||
GTK_WIDGET(state->summary_scroll), "summary", "5 — Confirmation");
|
||||
state->error = GTK_LABEL(gtk_label_new(NULL)); gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
||||
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_name(actions, "create-person-actions");
|
||||
|
|
@ -1900,10 +1962,9 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
g_task_run_in_thread(task, identity_languages_worker);
|
||||
g_object_unref(task);
|
||||
}
|
||||
gtk_window_present(state->window);
|
||||
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
|
||||
create_person_dialog_set_initial_ocr_paned_position,
|
||||
g_object_ref(ocr_paned), g_object_unref);
|
||||
labfy_paned_apply_initial_ratio(
|
||||
GTK_PANED(ocr_paned), 2.0 / 3.0, 520, 240);
|
||||
labfy_dialog_present(state->window);
|
||||
return TRUE;
|
||||
}
|
||||
void create_person_dialog_result_free(CreatePersonDialogResult *result)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
******************************************************************************/
|
||||
|
||||
#include "views/create_relation_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
#include "models/entity_record.h"
|
||||
#include "models/evidence_record.h"
|
||||
|
|
@ -1231,26 +1232,7 @@ static gboolean create_relation_dialog_present_internal(
|
|||
: "Ajouter une relation"
|
||||
);
|
||||
|
||||
gtk_window_set_transient_for(
|
||||
state->window,
|
||||
parent
|
||||
);
|
||||
|
||||
gtk_window_set_modal(
|
||||
state->window,
|
||||
TRUE
|
||||
);
|
||||
|
||||
gtk_window_set_destroy_with_parent(
|
||||
state->window,
|
||||
TRUE
|
||||
);
|
||||
|
||||
gtk_window_set_default_size(
|
||||
state->window,
|
||||
920,
|
||||
780
|
||||
);
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
|
||||
root_box =
|
||||
gtk_box_new(
|
||||
|
|
@ -1912,9 +1894,7 @@ static gboolean create_relation_dialog_present_internal(
|
|||
)
|
||||
);
|
||||
|
||||
gtk_window_present(
|
||||
state->window
|
||||
);
|
||||
labfy_dialog_present(state->window);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
******************************************************************************/
|
||||
|
||||
#include "views/create_social_account_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
struct CreateSocialAccountDialogResult
|
||||
{
|
||||
|
|
@ -200,9 +201,7 @@ gboolean create_social_account_dialog_present(
|
|||
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
|
||||
state->window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_title(state->window, "Ajouter un compte social");
|
||||
gtk_window_set_transient_for(state->window, parent);
|
||||
gtk_window_set_modal(state->window, TRUE);
|
||||
gtk_window_set_default_size(state->window, 620, 560);
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(box, 16); gtk_widget_set_margin_end(box, 16);
|
||||
gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16);
|
||||
|
|
@ -266,7 +265,7 @@ gboolean create_social_account_dialog_present(
|
|||
g_signal_connect(create, "clicked", G_CALLBACK(create_social_account_dialog_on_create), state);
|
||||
g_object_set_data_full(G_OBJECT(state->window), "social-account-state", state,
|
||||
create_social_account_dialog_state_free);
|
||||
gtk_window_present(state->window);
|
||||
labfy_dialog_present(state->window);
|
||||
g_free(now_text); g_date_time_unref(now);
|
||||
return TRUE;
|
||||
}
|
||||
|
|
|
|||
145
src/views/dialog_geometry.c
Normal file
145
src/views/dialog_geometry.c
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
#include "views/dialog_geometry.h"
|
||||
|
||||
#define LABFY_DIALOG_INITIAL_WIDTH 1200
|
||||
#define LABFY_DIALOG_INITIAL_HEIGHT 800
|
||||
#define LABFY_DIALOG_MINIMUM_WIDTH 800
|
||||
#define LABFY_DIALOG_MINIMUM_HEIGHT 600
|
||||
#define LABFY_DIALOG_WORKAREA_FACTOR 0.90
|
||||
|
||||
typedef struct {
|
||||
double ratio;
|
||||
int minimum_start;
|
||||
int minimum_end;
|
||||
} LabfyPanedGeometry;
|
||||
|
||||
LabfyDialogGeometry labfy_dialog_geometry_calculate(
|
||||
int workarea_width, int workarea_height)
|
||||
{
|
||||
LabfyDialogGeometry geometry = {0};
|
||||
int usable_width = workarea_width > 0
|
||||
? (workarea_width >= LABFY_DIALOG_MINIMUM_WIDTH
|
||||
? workarea_width
|
||||
: MAX(1, (int) (
|
||||
workarea_width * LABFY_DIALOG_WORKAREA_FACTOR)))
|
||||
: LABFY_DIALOG_INITIAL_WIDTH;
|
||||
int usable_height = workarea_height > 0
|
||||
? (workarea_height >= LABFY_DIALOG_MINIMUM_HEIGHT
|
||||
? workarea_height
|
||||
: MAX(1, (int) (
|
||||
workarea_height * LABFY_DIALOG_WORKAREA_FACTOR)))
|
||||
: LABFY_DIALOG_INITIAL_HEIGHT;
|
||||
geometry.initial_width = MIN(LABFY_DIALOG_INITIAL_WIDTH, usable_width);
|
||||
geometry.initial_height = MIN(LABFY_DIALOG_INITIAL_HEIGHT, usable_height);
|
||||
geometry.minimum_width = workarea_width >= LABFY_DIALOG_MINIMUM_WIDTH
|
||||
? LABFY_DIALOG_MINIMUM_WIDTH : usable_width;
|
||||
geometry.minimum_height = workarea_height >= LABFY_DIALOG_MINIMUM_HEIGHT
|
||||
? LABFY_DIALOG_MINIMUM_HEIGHT : usable_height;
|
||||
geometry.minimum_width = MIN(
|
||||
geometry.minimum_width, geometry.initial_width);
|
||||
geometry.minimum_height = MIN(
|
||||
geometry.minimum_height, geometry.initial_height);
|
||||
return geometry;
|
||||
}
|
||||
|
||||
static GdkMonitor *dialog_geometry_find_monitor(
|
||||
GtkWindow *window, GtkWindow *parent)
|
||||
{
|
||||
GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
|
||||
GtkNative *native = parent != NULL
|
||||
? gtk_widget_get_native(GTK_WIDGET(parent)) : NULL;
|
||||
GdkSurface *surface = native != NULL
|
||||
? gtk_native_get_surface(native) : NULL;
|
||||
if (surface != NULL) {
|
||||
GdkMonitor *monitor =
|
||||
gdk_display_get_monitor_at_surface(display, surface);
|
||||
if (monitor != NULL) return g_object_ref(monitor);
|
||||
}
|
||||
GListModel *monitors = gdk_display_get_monitors(display);
|
||||
return monitors != NULL && g_list_model_get_n_items(monitors) > 0
|
||||
? g_list_model_get_item(monitors, 0) : NULL;
|
||||
}
|
||||
|
||||
void labfy_dialog_apply_standard_geometry(
|
||||
GtkWindow *window, GtkWindow *parent)
|
||||
{
|
||||
GdkRectangle workarea = {0};
|
||||
GdkMonitor *monitor;
|
||||
LabfyDialogGeometry geometry;
|
||||
if (!GTK_IS_WINDOW(window)) return;
|
||||
monitor = dialog_geometry_find_monitor(window, parent);
|
||||
if (monitor != NULL) gdk_monitor_get_geometry(monitor, &workarea);
|
||||
geometry = labfy_dialog_geometry_calculate(
|
||||
workarea.width, workarea.height);
|
||||
gtk_window_set_resizable(window, TRUE);
|
||||
gtk_window_set_default_size(window,
|
||||
geometry.initial_width, geometry.initial_height);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(window),
|
||||
geometry.minimum_width, geometry.minimum_height);
|
||||
g_clear_object(&monitor);
|
||||
}
|
||||
|
||||
void labfy_dialog_prepare(
|
||||
GtkWindow *window,
|
||||
GtkWindow *parent,
|
||||
gboolean modal,
|
||||
gboolean destroy_with_parent)
|
||||
{
|
||||
if (!GTK_IS_WINDOW(window)) return;
|
||||
gtk_window_set_transient_for(window,
|
||||
GTK_IS_WINDOW(parent) ? parent : NULL);
|
||||
gtk_window_set_modal(window, modal);
|
||||
gtk_window_set_destroy_with_parent(window,
|
||||
destroy_with_parent && GTK_IS_WINDOW(parent));
|
||||
}
|
||||
|
||||
void labfy_dialog_present(GtkWindow *window)
|
||||
{
|
||||
GtkWindow *parent;
|
||||
|
||||
if (!GTK_IS_WINDOW(window)) return;
|
||||
parent = gtk_window_get_transient_for(window);
|
||||
labfy_dialog_apply_standard_geometry(window, parent);
|
||||
gtk_window_present(window);
|
||||
}
|
||||
|
||||
static void paned_geometry_apply(
|
||||
GtkPaned *paned, LabfyPanedGeometry *geometry)
|
||||
{
|
||||
int width = gtk_widget_get_width(GTK_WIDGET(paned));
|
||||
int position;
|
||||
if (width <= 0 || geometry == NULL) return;
|
||||
position = (int) (width * geometry->ratio);
|
||||
position = MAX(geometry->minimum_start, position);
|
||||
position = MIN(position, MAX(0, width - geometry->minimum_end));
|
||||
gtk_paned_set_position(paned, position);
|
||||
}
|
||||
|
||||
static gboolean paned_geometry_idle(gpointer data)
|
||||
{
|
||||
GtkPaned *paned = GTK_PANED(data);
|
||||
LabfyPanedGeometry *geometry = g_object_get_data(
|
||||
G_OBJECT(paned), "labfy-paned-initial-geometry");
|
||||
if (geometry == NULL) return G_SOURCE_REMOVE;
|
||||
if (gtk_widget_get_width(GTK_WIDGET(paned)) <= 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
paned_geometry_apply(paned, geometry);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void labfy_paned_apply_initial_ratio(
|
||||
GtkPaned *paned, double ratio, int minimum_start, int minimum_end)
|
||||
{
|
||||
LabfyPanedGeometry *geometry;
|
||||
if (!GTK_IS_PANED(paned)) return;
|
||||
if (g_object_get_data(
|
||||
G_OBJECT(paned), "labfy-paned-initial-geometry") != NULL)
|
||||
return;
|
||||
geometry = g_new0(LabfyPanedGeometry, 1);
|
||||
geometry->ratio = CLAMP(ratio, 0.1, 0.9);
|
||||
geometry->minimum_start = MAX(0, minimum_start);
|
||||
geometry->minimum_end = MAX(0, minimum_end);
|
||||
g_object_set_data_full(G_OBJECT(paned),
|
||||
"labfy-paned-initial-geometry", geometry, g_free);
|
||||
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, paned_geometry_idle,
|
||||
g_object_ref(paned), g_object_unref);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
* @brief Présentation en lecture seule d'une analyse EML.
|
||||
******************************************************************************/
|
||||
#include "views/eml_analysis_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
#include "widgets/controlled_vocab_dropdown.h"
|
||||
typedef struct { GtkWindow *window; GtkWidget *proposals_box;
|
||||
GtkWidget *summary_label;
|
||||
|
|
@ -234,8 +235,7 @@ void eml_analysis_dialog_present(GtkWindow *parent,
|
|||
state->callback = callback; state->user_data = user_data;
|
||||
window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_title(window, "Analyse locale du courriel EML");
|
||||
gtk_window_set_transient_for(window, parent); gtk_window_set_modal(window, TRUE);
|
||||
gtk_window_set_default_size(window, 720, 560);
|
||||
labfy_dialog_prepare(window, parent, TRUE, TRUE);
|
||||
state->window = window;
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(box, 14); gtk_widget_set_margin_end(box, 14);
|
||||
|
|
@ -314,7 +314,7 @@ void eml_analysis_dialog_present(GtkWindow *parent,
|
|||
G_CALLBACK(eml_analysis_dialog_on_window_close), state);
|
||||
g_object_set_data_full(G_OBJECT(window), "eml-dialog-state", state,
|
||||
eml_analysis_dialog_state_free);
|
||||
gtk_window_present(window);
|
||||
labfy_dialog_present(window);
|
||||
g_free(received); g_free(emails); g_free(domains);
|
||||
g_free(sender_ips); g_free(destination_ips);
|
||||
}
|
||||
|
|
@ -418,9 +418,7 @@ void eml_analysis_dialog_present_pipeline(
|
|||
state->callback = callback; state->user_data = user_data;
|
||||
state->window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_title(state->window, "Révision de l’analyse EML");
|
||||
gtk_window_set_transient_for(state->window, parent);
|
||||
gtk_window_set_modal(state->window, TRUE);
|
||||
gtk_window_set_default_size(state->window, 900, 700);
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
GtkWidget *outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(outer, 14);
|
||||
gtk_widget_set_margin_end(outer, 14);
|
||||
|
|
@ -538,7 +536,7 @@ void eml_analysis_dialog_present_pipeline(
|
|||
G_CALLBACK(eml_analysis_dialog_on_window_close), state);
|
||||
g_object_set_data_full(G_OBJECT(state->window), "eml-dialog-state",
|
||||
state, eml_analysis_dialog_state_free);
|
||||
gtk_window_present(state->window);
|
||||
labfy_dialog_present(state->window);
|
||||
g_free(received);
|
||||
g_string_free(attachments, TRUE);
|
||||
g_string_free(texts, TRUE);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "views/evidence_identity_ocr_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
#include "core/file_hash.h"
|
||||
#include "core/identity_ocr_workflow.h"
|
||||
|
|
@ -129,24 +130,6 @@ static void on_reset_transcription(GtkButton *button, gpointer data)
|
|||
gtk_widget_set_sensitive(GTK_WIDGET(state->save_transcription), TRUE);
|
||||
}
|
||||
|
||||
static gboolean set_initial_paned_position(gpointer data)
|
||||
{
|
||||
GtkWidget *paned = data;
|
||||
int width;
|
||||
int position;
|
||||
if (g_object_get_data(
|
||||
G_OBJECT(paned), "identity-initial-position-set") != NULL)
|
||||
return G_SOURCE_REMOVE;
|
||||
width = gtk_widget_get_width(paned);
|
||||
if (width <= 0) return G_SOURCE_CONTINUE;
|
||||
position = MAX(520, (width * 2) / 3);
|
||||
position = MIN(position, MAX(0, width - 180));
|
||||
gtk_paned_set_position(GTK_PANED(paned), position);
|
||||
g_object_set_data(G_OBJECT(paned),
|
||||
"identity-initial-position-set", GINT_TO_POINTER(1));
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static const char *mime_from_path(const char *path)
|
||||
{
|
||||
const char *dot = path != NULL ? strrchr(path, '.') : NULL;
|
||||
|
|
@ -545,6 +528,8 @@ static void workflow_completed(GObject *source, GAsyncResult *result,
|
|||
GTK_WIDGET(state->save_transcription), TRUE);
|
||||
ocr_provenance_overlay_set_image(state->overlay,
|
||||
identity_ocr_run_get_preview(state->run), state->generation);
|
||||
ocr_provenance_overlay_set_page(state->overlay,
|
||||
identity_ocr_run_get_page(state->run), state->generation);
|
||||
gtk_label_set_text(state->status,
|
||||
"OCR terminé. Vérifiez chaque proposition.");
|
||||
gtk_widget_set_sensitive(
|
||||
|
|
@ -668,11 +653,9 @@ gboolean evidence_identity_ocr_dialog_present(
|
|||
state->window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_application(state->window,
|
||||
gtk_window_get_application(parent));
|
||||
gtk_window_set_transient_for(state->window, parent);
|
||||
gtk_window_set_modal(state->window, TRUE);
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
gtk_window_set_title(state->window,
|
||||
"Import — OCR d’identité facultatif");
|
||||
gtk_window_set_default_size(state->window, 1200, 800);
|
||||
GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_name(root, "identity-form-content");
|
||||
gtk_widget_set_margin_start(root, 14);
|
||||
|
|
@ -881,9 +864,9 @@ gboolean evidence_identity_ocr_dialog_present(
|
|||
state->tesseract_path != NULL);
|
||||
if (state->tesseract_path != NULL &&
|
||||
!populate_languages(state, error)) goto failure_window;
|
||||
gtk_window_present(state->window);
|
||||
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
|
||||
set_initial_paned_position, g_object_ref(paned), g_object_unref);
|
||||
labfy_paned_apply_initial_ratio(
|
||||
GTK_PANED(paned), 2.0 / 3.0, 520, 240);
|
||||
labfy_dialog_present(state->window);
|
||||
return TRUE;
|
||||
|
||||
failure_window:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
******************************************************************************/
|
||||
|
||||
#include "views/evidence_import_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
#include "models/evidence_type.h"
|
||||
#include "core/file_hash.h"
|
||||
#include "core/task_manager.h"
|
||||
|
|
@ -974,10 +975,7 @@ gboolean evidence_import_dialog_present(
|
|||
"Importer une preuve"
|
||||
);
|
||||
|
||||
gtk_window_set_transient_for(
|
||||
state->window,
|
||||
parent
|
||||
);
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
|
||||
gtk_window_set_application(
|
||||
state->window,
|
||||
|
|
@ -986,22 +984,6 @@ gboolean evidence_import_dialog_present(
|
|||
)
|
||||
);
|
||||
|
||||
gtk_window_set_modal(
|
||||
state->window,
|
||||
TRUE
|
||||
);
|
||||
|
||||
gtk_window_set_destroy_with_parent(
|
||||
state->window,
|
||||
TRUE
|
||||
);
|
||||
|
||||
gtk_window_set_default_size(
|
||||
state->window,
|
||||
1200,
|
||||
800
|
||||
);
|
||||
|
||||
root_box =
|
||||
gtk_box_new(
|
||||
GTK_ORIENTATION_VERTICAL,
|
||||
|
|
@ -1411,10 +1393,26 @@ gboolean evidence_import_dialog_present(
|
|||
button_box
|
||||
);
|
||||
|
||||
GtkWidget *left_panel = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
GtkWidget *form_scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_name(form_scroll, "evidence-import-form-scroll");
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(form_scroll),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(form_scroll, TRUE);
|
||||
g_object_ref(button_box);
|
||||
gtk_box_remove(GTK_BOX(root_box), button_box);
|
||||
gtk_scrolled_window_set_child(
|
||||
GTK_SCROLLED_WINDOW(form_scroll), root_box);
|
||||
gtk_box_append(GTK_BOX(left_panel), form_scroll);
|
||||
gtk_widget_set_name(button_box, "evidence-import-actions");
|
||||
gtk_widget_set_margin_end(button_box, 18);
|
||||
gtk_widget_set_margin_bottom(button_box, 12);
|
||||
gtk_box_append(GTK_BOX(left_panel), button_box);
|
||||
g_object_unref(button_box);
|
||||
paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_widget_set_name(paned, "evidence-import-paned");
|
||||
gtk_paned_set_start_child(GTK_PANED(paned), root_box);
|
||||
gtk_widget_set_size_request(root_box, 320, -1);
|
||||
gtk_paned_set_start_child(GTK_PANED(paned), left_panel);
|
||||
gtk_widget_set_size_request(left_panel, 320, -1);
|
||||
state->preview_task_manager = task_manager_new();
|
||||
state->preview = evidence_preview_widget_new(
|
||||
state->preview_task_manager, NULL, NULL);
|
||||
|
|
@ -1425,7 +1423,8 @@ gboolean evidence_import_dialog_present(
|
|||
}
|
||||
gtk_paned_set_end_child(GTK_PANED(paned),
|
||||
evidence_preview_widget_get_widget(state->preview));
|
||||
gtk_paned_set_position(GTK_PANED(paned), 420);
|
||||
labfy_paned_apply_initial_ratio(
|
||||
GTK_PANED(paned), 2.0 / 3.0, 480, 240);
|
||||
gtk_paned_set_resize_start_child(GTK_PANED(paned), FALSE);
|
||||
gtk_paned_set_resize_end_child(GTK_PANED(paned), TRUE);
|
||||
gtk_window_set_child(state->window, paned);
|
||||
|
|
@ -1500,9 +1499,7 @@ gboolean evidence_import_dialog_present(
|
|||
import_button
|
||||
);
|
||||
|
||||
gtk_window_present(
|
||||
state->window
|
||||
);
|
||||
labfy_dialog_present(state->window);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
******************************************************************************/
|
||||
|
||||
#include "views/evidence_metadata_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
#include "models/evidence_type.h"
|
||||
#include "dao/identity_ocr_dao.h"
|
||||
|
|
@ -258,6 +259,8 @@ static void evidence_metadata_dialog_render_ocr(
|
|||
|
||||
IdentityOcrRunRecord *run = g_ptr_array_index(
|
||||
context->ocr_runs, selected);
|
||||
ocr_provenance_overlay_set_page(context->ocr_overlay,
|
||||
(guint) run->page_number, context->ocr_generation);
|
||||
const char *selected_identifier = g_ptr_array_index(
|
||||
context->ocr_run_identifiers, selected);
|
||||
if (g_strcmp0(run->id, selected_identifier) != 0) return;
|
||||
|
|
@ -492,13 +495,10 @@ gboolean evidence_metadata_dialog_present_with_ocr(
|
|||
evidence_record_get_type_identifier(record)) == 0) selected = index;
|
||||
}
|
||||
gtk_window_set_title(context->window, "Modifier les métadonnées");
|
||||
gtk_window_set_default_size(context->window, 560, 420);
|
||||
gtk_window_set_modal(context->window, TRUE);
|
||||
gtk_window_set_destroy_with_parent(context->window, TRUE);
|
||||
labfy_dialog_prepare(context->window, parent, TRUE, TRUE);
|
||||
if (parent != NULL) {
|
||||
gtk_window_set_application(context->window,
|
||||
gtk_window_get_application(parent));
|
||||
gtk_window_set_transient_for(context->window, parent);
|
||||
}
|
||||
g_object_set_data_full(G_OBJECT(context->window), "evidence-metadata-context",
|
||||
context, evidence_metadata_dialog_context_free);
|
||||
|
|
@ -577,7 +577,7 @@ gboolean evidence_metadata_dialog_present_with_ocr(
|
|||
gtk_box_append(GTK_BOX(buttons), cancel); gtk_box_append(GTK_BOX(buttons), save);
|
||||
gtk_box_append(GTK_BOX(main_box), buttons);
|
||||
gtk_window_set_child(context->window, main_box);
|
||||
gtk_window_present(context->window);
|
||||
labfy_dialog_present(context->window);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* @brief Dialogue de sélection des preuves associées à une entité.
|
||||
******************************************************************************/
|
||||
#include "views/manage_entity_evidence_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
#include "widgets/evidence_preview_widget.h"
|
||||
#include <gio/gio.h>
|
||||
typedef struct
|
||||
|
|
@ -116,9 +117,7 @@ gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
|||
state->import_callback = import_callback;
|
||||
state->window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_title(state->window, "Pièces jointes de la personne");
|
||||
gtk_window_set_transient_for(state->window, parent);
|
||||
gtk_window_set_modal(state->window, TRUE);
|
||||
gtk_window_set_default_size(state->window, 1200, 800);
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
gtk_widget_set_margin_top(root, 16); gtk_widget_set_margin_bottom(root, 16);
|
||||
gtk_widget_set_margin_start(root, 16); gtk_widget_set_margin_end(root, 16);
|
||||
|
|
@ -178,7 +177,8 @@ gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
|||
gtk_paned_set_start_child(GTK_PANED(content), scroll);
|
||||
gtk_paned_set_end_child(GTK_PANED(content),
|
||||
evidence_preview_widget_get_widget(state->preview));
|
||||
gtk_paned_set_position(GTK_PANED(content), 380);
|
||||
labfy_paned_apply_initial_ratio(
|
||||
GTK_PANED(content), 2.0 / 3.0, 480, 240);
|
||||
gtk_paned_set_resize_start_child(GTK_PANED(content), FALSE);
|
||||
gtk_paned_set_resize_end_child(GTK_PANED(content), TRUE);
|
||||
save = gtk_button_new_with_label("Enregistrer les pièces jointes");
|
||||
|
|
@ -194,5 +194,5 @@ gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
|||
g_signal_connect(save, "clicked", G_CALLBACK(manage_evidence_on_save), state);
|
||||
g_signal_connect(import, "clicked", G_CALLBACK(manage_evidence_on_import), state);
|
||||
g_signal_connect(state->window, "close-request", G_CALLBACK(manage_evidence_on_close), state);
|
||||
gtk_window_present(state->window); return TRUE;
|
||||
labfy_dialog_present(state->window); return TRUE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
******************************************************************************/
|
||||
|
||||
#include "views/metadata_analysis_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
static gboolean metadata_analysis_dialog_on_escape(
|
||||
GtkWidget *widget, GVariant *arguments, gpointer user_data)
|
||||
|
|
@ -30,9 +31,7 @@ void metadata_analysis_dialog_present(GtkWindow *parent, const char *summary,
|
|||
|
||||
window = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(window), "Métadonnées de la preuve");
|
||||
gtk_window_set_default_size(GTK_WINDOW(window), 760, 560);
|
||||
gtk_window_set_modal(GTK_WINDOW(window), TRUE);
|
||||
if (parent != NULL) gtk_window_set_transient_for(GTK_WINDOW(window), parent);
|
||||
labfy_dialog_prepare(GTK_WINDOW(window), parent, TRUE, TRUE);
|
||||
content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
gtk_widget_set_margin_top(content, 16);
|
||||
gtk_widget_set_margin_bottom(content, 16);
|
||||
|
|
@ -81,7 +80,7 @@ void metadata_analysis_dialog_present(GtkWindow *parent, const char *summary,
|
|||
}
|
||||
gtk_box_append(GTK_BOX(content), close_button);
|
||||
gtk_window_set_child(GTK_WINDOW(window), content);
|
||||
gtk_window_present(GTK_WINDOW(window));
|
||||
labfy_dialog_present(GTK_WINDOW(window));
|
||||
g_free(heading_text);
|
||||
g_free(path_text);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
******************************************************************************/
|
||||
|
||||
#include "views/osint_dns_review_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
#include "models/osint_dns_proposal.h"
|
||||
|
||||
|
|
@ -211,11 +212,7 @@ void osint_dns_review_dialog_present(
|
|||
context->user_data = user_data;
|
||||
|
||||
gtk_window_set_title(context->window, "Intégrer les propositions DNS");
|
||||
gtk_window_set_default_size(context->window, 720, 560);
|
||||
gtk_window_set_modal(context->window, TRUE);
|
||||
gtk_window_set_destroy_with_parent(context->window, TRUE);
|
||||
if (parent_window != NULL)
|
||||
gtk_window_set_transient_for(context->window, parent_window);
|
||||
labfy_dialog_prepare(context->window, parent_window, TRUE, TRUE);
|
||||
g_object_set_data_full(
|
||||
G_OBJECT(context->window), "osint-dns-review-context", context,
|
||||
osint_dns_review_dialog_context_free
|
||||
|
|
@ -279,5 +276,5 @@ void osint_dns_review_dialog_present(
|
|||
gtk_box_append(GTK_BOX(main_box), scrolled_window);
|
||||
gtk_box_append(GTK_BOX(main_box), button_box);
|
||||
gtk_window_set_child(context->window, main_box);
|
||||
gtk_window_present(context->window);
|
||||
labfy_dialog_present(context->window);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
******************************************************************************/
|
||||
|
||||
#include "views/osint_execution_history_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
#include "core/osint_execution_integrity.h"
|
||||
#include "models/osint_execution_record.h"
|
||||
|
|
@ -153,10 +154,7 @@ void osint_execution_history_dialog_present(
|
|||
context->records = g_ptr_array_ref(records);
|
||||
context->linked_objects = g_hash_table_ref(linked_objects);
|
||||
gtk_window_set_title(context->window, "Historique OSINT");
|
||||
gtk_window_set_default_size(context->window, 980, 650);
|
||||
gtk_window_set_modal(context->window, TRUE);
|
||||
gtk_window_set_destroy_with_parent(context->window, TRUE);
|
||||
if (parent_window != NULL) gtk_window_set_transient_for(context->window, parent_window);
|
||||
labfy_dialog_prepare(context->window, parent_window, TRUE, TRUE);
|
||||
g_object_set_data_full(G_OBJECT(context->window), "osint-history-context",
|
||||
context, osint_execution_history_dialog_context_free);
|
||||
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
|
|
@ -214,5 +212,5 @@ void osint_execution_history_dialog_present(
|
|||
gtk_window_set_child(context->window, main_box);
|
||||
osint_execution_history_dialog_on_record_clicked(
|
||||
GTK_BUTTON(gtk_widget_get_first_child(history_box)), context);
|
||||
gtk_window_present(context->window);
|
||||
labfy_dialog_present(context->window);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* @brief Révision humaine des résultats OCR d'un RIB.
|
||||
******************************************************************************/
|
||||
#include "views/rib_ocr_review_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
#include "core/iban_analyzer.h"
|
||||
typedef struct { GtkWindow *window; GtkEntry *iban; GtkLabel *error;
|
||||
RibOcrReviewCallback callback; gpointer data; gboolean completed; } State;
|
||||
|
|
@ -36,8 +37,7 @@ void rib_ocr_review_dialog_present(GtkWindow *parent, const char *ocr_text,
|
|||
state->data = user_data; state->iban = GTK_ENTRY(gtk_entry_new());
|
||||
state->error = GTK_LABEL(gtk_label_new(NULL));
|
||||
gtk_window_set_title(state->window, "Réviser l’analyse OCR du RIB");
|
||||
gtk_window_set_transient_for(state->window, parent); gtk_window_set_modal(state->window, TRUE);
|
||||
gtk_window_set_default_size(state->window, 760, 620);
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
gtk_widget_set_margin_top(root, 16); gtk_widget_set_margin_bottom(root, 16);
|
||||
gtk_widget_set_margin_start(root, 16); gtk_widget_set_margin_end(root, 16);
|
||||
gtk_text_view_set_editable(GTK_TEXT_VIEW(view), FALSE);
|
||||
|
|
@ -55,5 +55,5 @@ void rib_ocr_review_dialog_present(GtkWindow *parent, const char *ocr_text,
|
|||
g_object_set_data_full(G_OBJECT(state->window), "state", state, state_free);
|
||||
g_signal_connect(confirm, "clicked", G_CALLBACK(on_confirm), state);
|
||||
g_signal_connect(state->window, "close-request", G_CALLBACK(on_close), state);
|
||||
gtk_window_present(state->window);
|
||||
labfy_dialog_present(state->window);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,19 @@ struct EvidencePreviewWidget {
|
|||
GtkWidget *root;
|
||||
GtkLabel *metadata;
|
||||
GtkLabel *status;
|
||||
GtkWidget *toolbar;
|
||||
GtkStack *stack;
|
||||
GtkPicture *image;
|
||||
GtkPicture *pdf;
|
||||
GtkScrolledWindow *image_scroll;
|
||||
GtkScrolledWindow *pdf_scroll;
|
||||
GtkButton *zoom_out;
|
||||
GtkButton *fit;
|
||||
GtkButton *zoom_in;
|
||||
GtkLabel *zoom_label;
|
||||
GtkButton *previous_page;
|
||||
GtkButton *next_page;
|
||||
GtkLabel *page_label;
|
||||
GtkVideo *video;
|
||||
GtkTextView *text;
|
||||
GtkTextView *email;
|
||||
|
|
@ -19,6 +29,16 @@ struct EvidencePreviewWidget {
|
|||
gpointer session_data;
|
||||
guint64 generation;
|
||||
char *identifier;
|
||||
EvidencePreviewRequest *request;
|
||||
char *metadata_text;
|
||||
gint raster_width;
|
||||
gint raster_height;
|
||||
guint zoom_index;
|
||||
gboolean fit_mode;
|
||||
gboolean raster_available;
|
||||
gboolean pdf_available;
|
||||
guint pdf_page;
|
||||
guint pdf_page_count;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -77,6 +97,148 @@ static void preview_widget_reset_content(EvidencePreviewWidget *widget)
|
|||
gtk_text_buffer_set_text(gtk_text_view_get_buffer(widget->email), "", -1);
|
||||
}
|
||||
|
||||
static const double zoom_factors[] = {
|
||||
0.25, 0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 4.00
|
||||
};
|
||||
|
||||
static GtkPicture *preview_widget_active_picture(
|
||||
EvidencePreviewWidget *widget)
|
||||
{
|
||||
return widget->pdf_available ? widget->pdf : widget->image;
|
||||
}
|
||||
|
||||
static GtkScrolledWindow *preview_widget_active_scroll(
|
||||
EvidencePreviewWidget *widget)
|
||||
{
|
||||
return widget->pdf_available ? widget->pdf_scroll : widget->image_scroll;
|
||||
}
|
||||
|
||||
static void preview_widget_update_page_controls(
|
||||
EvidencePreviewWidget *widget)
|
||||
{
|
||||
char *text = g_strdup_printf("Page %u / %u",
|
||||
widget->pdf_page_count > 0 ? widget->pdf_page + 1U : 0U,
|
||||
widget->pdf_page_count);
|
||||
gtk_label_set_text(widget->page_label, text);
|
||||
g_free(text);
|
||||
gtk_widget_set_visible(GTK_WIDGET(widget->previous_page),
|
||||
widget->pdf_available);
|
||||
gtk_widget_set_visible(GTK_WIDGET(widget->next_page),
|
||||
widget->pdf_available);
|
||||
gtk_widget_set_visible(GTK_WIDGET(widget->page_label),
|
||||
widget->pdf_available);
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(widget->previous_page),
|
||||
widget->pdf_available && widget->pdf_page > 0);
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(widget->next_page),
|
||||
widget->pdf_available &&
|
||||
widget->pdf_page + 1U < widget->pdf_page_count);
|
||||
}
|
||||
|
||||
static void preview_widget_apply_zoom(EvidencePreviewWidget *widget)
|
||||
{
|
||||
GtkPicture *picture;
|
||||
GtkScrolledWindow *scroll;
|
||||
double factor;
|
||||
char *text;
|
||||
if (widget == NULL) return;
|
||||
picture = preview_widget_active_picture(widget);
|
||||
scroll = preview_widget_active_scroll(widget);
|
||||
gtk_widget_set_visible(GTK_WIDGET(widget->zoom_out),
|
||||
widget->raster_available);
|
||||
gtk_widget_set_visible(GTK_WIDGET(widget->fit),
|
||||
widget->raster_available);
|
||||
gtk_widget_set_visible(GTK_WIDGET(widget->zoom_in),
|
||||
widget->raster_available);
|
||||
gtk_widget_set_visible(GTK_WIDGET(widget->zoom_label),
|
||||
widget->raster_available);
|
||||
if (!widget->raster_available) return;
|
||||
factor = zoom_factors[widget->zoom_index];
|
||||
text = widget->fit_mode ? g_strdup("Ajusté") :
|
||||
g_strdup_printf("%.0f %%", factor * 100.0);
|
||||
gtk_label_set_text(widget->zoom_label, text);
|
||||
g_free(text);
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(widget->zoom_out),
|
||||
widget->fit_mode || widget->zoom_index > 0);
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(widget->zoom_in),
|
||||
widget->fit_mode ||
|
||||
widget->zoom_index + 1U < G_N_ELEMENTS(zoom_factors));
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(widget->fit),
|
||||
!widget->fit_mode);
|
||||
if (widget->fit_mode) {
|
||||
gtk_picture_set_can_shrink(picture, TRUE);
|
||||
gtk_picture_set_content_fit(picture, GTK_CONTENT_FIT_CONTAIN);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(picture), -1, -1);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(picture), TRUE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(picture), TRUE);
|
||||
gtk_scrolled_window_set_policy(scroll,
|
||||
GTK_POLICY_NEVER, GTK_POLICY_NEVER);
|
||||
} else {
|
||||
gtk_picture_set_can_shrink(picture, FALSE);
|
||||
gtk_picture_set_content_fit(picture, GTK_CONTENT_FIT_CONTAIN);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(picture), FALSE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(picture), FALSE);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(picture),
|
||||
MAX(1, (gint) (widget->raster_width * factor)),
|
||||
MAX(1, (gint) (widget->raster_height * factor)));
|
||||
gtk_scrolled_window_set_policy(scroll,
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
}
|
||||
}
|
||||
|
||||
void evidence_preview_widget_zoom_in(EvidencePreviewWidget *widget)
|
||||
{
|
||||
if (widget == NULL || !widget->raster_available) return;
|
||||
if (widget->fit_mode) {
|
||||
widget->fit_mode = FALSE;
|
||||
widget->zoom_index = 4;
|
||||
} else if (widget->zoom_index + 1U < G_N_ELEMENTS(zoom_factors))
|
||||
widget->zoom_index++;
|
||||
preview_widget_apply_zoom(widget);
|
||||
}
|
||||
|
||||
void evidence_preview_widget_zoom_out(EvidencePreviewWidget *widget)
|
||||
{
|
||||
if (widget == NULL || !widget->raster_available) return;
|
||||
if (widget->fit_mode) {
|
||||
widget->fit_mode = FALSE;
|
||||
widget->zoom_index = 2;
|
||||
} else if (widget->zoom_index > 0)
|
||||
widget->zoom_index--;
|
||||
preview_widget_apply_zoom(widget);
|
||||
}
|
||||
|
||||
void evidence_preview_widget_fit(EvidencePreviewWidget *widget)
|
||||
{
|
||||
if (widget == NULL || !widget->raster_available) return;
|
||||
widget->fit_mode = TRUE;
|
||||
preview_widget_apply_zoom(widget);
|
||||
}
|
||||
|
||||
double evidence_preview_widget_get_zoom(
|
||||
const EvidencePreviewWidget *widget)
|
||||
{
|
||||
return widget != NULL && !widget->fit_mode
|
||||
? zoom_factors[widget->zoom_index] : 0.0;
|
||||
}
|
||||
|
||||
gboolean evidence_preview_widget_is_fit(
|
||||
const EvidencePreviewWidget *widget)
|
||||
{
|
||||
return widget != NULL && widget->fit_mode;
|
||||
}
|
||||
|
||||
guint evidence_preview_widget_get_pdf_page(
|
||||
const EvidencePreviewWidget *widget)
|
||||
{
|
||||
return widget != NULL ? widget->pdf_page : 0;
|
||||
}
|
||||
|
||||
guint evidence_preview_widget_get_pdf_page_count(
|
||||
const EvidencePreviewWidget *widget)
|
||||
{
|
||||
return widget != NULL ? widget->pdf_page_count : 0;
|
||||
}
|
||||
|
||||
void evidence_preview_widget_cancel(EvidencePreviewWidget *widget)
|
||||
{
|
||||
if (widget == NULL) return;
|
||||
|
|
@ -89,6 +251,8 @@ void evidence_preview_widget_cancel(EvidencePreviewWidget *widget)
|
|||
if (widget->root != NULL)
|
||||
preview_widget_reset_content(widget);
|
||||
g_clear_pointer(&widget->identifier, g_free);
|
||||
g_clear_pointer(&widget->request, evidence_preview_request_free);
|
||||
g_clear_pointer(&widget->metadata_text, g_free);
|
||||
}
|
||||
|
||||
void evidence_preview_widget_clear(EvidencePreviewWidget *widget)
|
||||
|
|
@ -98,6 +262,12 @@ void evidence_preview_widget_clear(EvidencePreviewWidget *widget)
|
|||
gtk_label_set_text(widget->metadata, "");
|
||||
gtk_label_set_text(widget->status, "Aucune preuve sélectionnée.");
|
||||
gtk_stack_set_visible_child_name(widget->stack, "empty");
|
||||
widget->raster_available = FALSE;
|
||||
widget->pdf_available = FALSE;
|
||||
widget->pdf_page = 0;
|
||||
widget->pdf_page_count = 0;
|
||||
preview_widget_apply_zoom(widget);
|
||||
preview_widget_update_page_controls(widget);
|
||||
}
|
||||
|
||||
static void preview_widget_completed(BackgroundTask *task, gpointer data)
|
||||
|
|
@ -128,9 +298,25 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
|
|||
result->kind == EVIDENCE_PREVIEW_KIND_PDF
|
||||
? "pdf" : "image");
|
||||
gtk_label_set_text(widget->status, "Aperçu intègre.");
|
||||
widget->raster_available = TRUE;
|
||||
widget->pdf_available =
|
||||
result->kind == EVIDENCE_PREVIEW_KIND_PDF;
|
||||
widget->raster_width = result->width;
|
||||
widget->raster_height = result->height;
|
||||
if (widget->pdf_available) {
|
||||
widget->pdf_page_count = result->item_count;
|
||||
widget->pdf_page = result->current_page - 1U;
|
||||
} else {
|
||||
widget->pdf_page_count = 0;
|
||||
widget->pdf_page = 0;
|
||||
}
|
||||
preview_widget_apply_zoom(widget);
|
||||
preview_widget_update_page_controls(widget);
|
||||
g_object_unref(texture);
|
||||
}
|
||||
} else if (result->kind == EVIDENCE_PREVIEW_KIND_VIDEO) {
|
||||
widget->raster_available = FALSE;
|
||||
widget->pdf_available = FALSE;
|
||||
GtkMediaStream *media = gtk_media_file_new_for_filename(
|
||||
result->controlled_path);
|
||||
gtk_media_stream_set_volume(media, 0.0);
|
||||
|
|
@ -145,6 +331,8 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
|
|||
gtk_label_set_text(widget->status, result->message);
|
||||
} else if (result->kind == EVIDENCE_PREVIEW_KIND_TEXT ||
|
||||
result->kind == EVIDENCE_PREVIEW_KIND_EMAIL) {
|
||||
widget->raster_available = FALSE;
|
||||
widget->pdf_available = FALSE;
|
||||
GtkTextView *view = result->kind == EVIDENCE_PREVIEW_KIND_EMAIL
|
||||
? widget->email : widget->text;
|
||||
gtk_text_buffer_set_text(gtk_text_view_get_buffer(view),
|
||||
|
|
@ -155,6 +343,8 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
|
|||
gtk_label_set_text(widget->status,
|
||||
result->truncated ? "Aperçu tronqué." : "Aperçu intègre.");
|
||||
} else {
|
||||
widget->raster_available = FALSE;
|
||||
widget->pdf_available = FALSE;
|
||||
gtk_stack_set_visible_child_name(widget->stack,
|
||||
result->kind == EVIDENCE_PREVIEW_KIND_UNSUPPORTED
|
||||
? "unsupported" : "error");
|
||||
|
|
@ -162,6 +352,10 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
|
|||
result->message != NULL ? result->message :
|
||||
"Aperçu illisible.");
|
||||
}
|
||||
if (!widget->raster_available) {
|
||||
preview_widget_apply_zoom(widget);
|
||||
preview_widget_update_page_controls(widget);
|
||||
}
|
||||
if (error != NULL) {
|
||||
gtk_stack_set_visible_child_name(widget->stack, "error");
|
||||
gtk_label_set_text(widget->status, error->message);
|
||||
|
|
@ -182,6 +376,100 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
|
|||
g_clear_object(&root);
|
||||
}
|
||||
|
||||
static void preview_widget_start_request(EvidencePreviewWidget *widget,
|
||||
guint pdf_page)
|
||||
{
|
||||
EvidencePreviewRequest *owned;
|
||||
PreviewWidgetContext *context;
|
||||
GError *error = NULL;
|
||||
if (widget == NULL || widget->request == NULL) return;
|
||||
widget->generation++;
|
||||
if (widget->task != NULL) {
|
||||
background_task_cancel(widget->task);
|
||||
background_task_unref(widget->task);
|
||||
widget->task = NULL;
|
||||
}
|
||||
owned = evidence_preview_request_new(
|
||||
widget->request->investigation_root_path,
|
||||
widget->request->evidence_identifier,
|
||||
widget->request->relative_path,
|
||||
widget->request->expected_sha256,
|
||||
widget->request->mime_type, widget->generation);
|
||||
evidence_preview_request_set_pdf_page(owned, pdf_page);
|
||||
gtk_label_set_text(widget->metadata,
|
||||
widget->metadata_text != NULL ? widget->metadata_text : "");
|
||||
gtk_label_set_text(widget->status,
|
||||
"Détection, vérification et chargement en cours…");
|
||||
gtk_stack_set_visible_child_name(widget->stack, "loading");
|
||||
context = g_new0(PreviewWidgetContext, 1);
|
||||
g_weak_ref_init(&context->root, G_OBJECT(widget->root));
|
||||
context->identifier = g_strdup(widget->identifier);
|
||||
context->generation = widget->generation;
|
||||
if (owned != NULL)
|
||||
widget->task = evidence_preview_task_start(widget->task_manager,
|
||||
owned, preview_widget_completed, context,
|
||||
preview_widget_context_free, &error);
|
||||
evidence_preview_request_free(owned);
|
||||
if (widget->task == NULL) {
|
||||
preview_widget_context_free(context);
|
||||
gtk_stack_set_visible_child_name(widget->stack, "error");
|
||||
gtk_label_set_text(widget->status,
|
||||
error != NULL ? error->message : "Aperçu impossible.");
|
||||
}
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
gboolean evidence_preview_widget_set_pdf_page(
|
||||
EvidencePreviewWidget *widget, guint page)
|
||||
{
|
||||
if (widget == NULL || !widget->pdf_available ||
|
||||
page >= widget->pdf_page_count || page == widget->pdf_page)
|
||||
return FALSE;
|
||||
widget->pdf_page = page;
|
||||
preview_widget_update_page_controls(widget);
|
||||
preview_widget_start_request(widget, page);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void preview_widget_on_zoom_out(
|
||||
GtkButton *button, gpointer data)
|
||||
{
|
||||
(void) button;
|
||||
evidence_preview_widget_zoom_out(data);
|
||||
}
|
||||
|
||||
static void preview_widget_on_fit(GtkButton *button, gpointer data)
|
||||
{
|
||||
(void) button;
|
||||
evidence_preview_widget_fit(data);
|
||||
}
|
||||
|
||||
static void preview_widget_on_zoom_in(
|
||||
GtkButton *button, gpointer data)
|
||||
{
|
||||
(void) button;
|
||||
evidence_preview_widget_zoom_in(data);
|
||||
}
|
||||
|
||||
static void preview_widget_on_previous(
|
||||
GtkButton *button, gpointer data)
|
||||
{
|
||||
EvidencePreviewWidget *widget = data;
|
||||
(void) button;
|
||||
if (widget->pdf_page > 0)
|
||||
evidence_preview_widget_set_pdf_page(
|
||||
widget, widget->pdf_page - 1U);
|
||||
}
|
||||
|
||||
static void preview_widget_on_next(GtkButton *button, gpointer data)
|
||||
{
|
||||
EvidencePreviewWidget *widget = data;
|
||||
(void) button;
|
||||
if (widget->pdf_page + 1U < widget->pdf_page_count)
|
||||
evidence_preview_widget_set_pdf_page(
|
||||
widget, widget->pdf_page + 1U);
|
||||
}
|
||||
|
||||
EvidencePreviewWidget *evidence_preview_widget_new(
|
||||
TaskManager *task_manager,
|
||||
EvidencePreviewWidgetSessionCheck session_check,
|
||||
|
|
@ -199,7 +487,10 @@ EvidencePreviewWidget *evidence_preview_widget_new(
|
|||
widget->task_manager = task_manager;
|
||||
widget->session_check = session_check;
|
||||
widget->session_data = session_data;
|
||||
widget->fit_mode = TRUE;
|
||||
widget->zoom_index = 3;
|
||||
widget->root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
gtk_widget_set_name(widget->root, "evidence-preview-widget");
|
||||
g_object_weak_ref(G_OBJECT(widget->root),
|
||||
preview_widget_root_finalized, widget);
|
||||
widget->metadata = GTK_LABEL(gtk_label_new(""));
|
||||
|
|
@ -207,6 +498,65 @@ EvidencePreviewWidget *evidence_preview_widget_new(
|
|||
gtk_label_set_selectable(widget->metadata, TRUE);
|
||||
widget->status = GTK_LABEL(gtk_label_new("Aucune preuve sélectionnée."));
|
||||
gtk_label_set_xalign(widget->status, 0.0f);
|
||||
widget->toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_set_name(widget->toolbar, "evidence-preview-toolbar");
|
||||
widget->zoom_out = GTK_BUTTON(gtk_button_new_from_icon_name(
|
||||
"zoom-out-symbolic"));
|
||||
widget->fit = GTK_BUTTON(gtk_button_new_from_icon_name(
|
||||
"zoom-fit-best-symbolic"));
|
||||
widget->zoom_in = GTK_BUTTON(gtk_button_new_from_icon_name(
|
||||
"zoom-in-symbolic"));
|
||||
widget->previous_page = GTK_BUTTON(gtk_button_new_from_icon_name(
|
||||
"go-previous-symbolic"));
|
||||
widget->next_page = GTK_BUTTON(gtk_button_new_from_icon_name(
|
||||
"go-next-symbolic"));
|
||||
widget->zoom_label = GTK_LABEL(gtk_label_new("Ajusté"));
|
||||
widget->page_label = GTK_LABEL(gtk_label_new("Page 0 / 0"));
|
||||
gtk_widget_set_name(GTK_WIDGET(widget->zoom_out),
|
||||
"evidence-preview-zoom-out");
|
||||
gtk_widget_set_name(GTK_WIDGET(widget->fit),
|
||||
"evidence-preview-fit");
|
||||
gtk_widget_set_name(GTK_WIDGET(widget->zoom_in),
|
||||
"evidence-preview-zoom-in");
|
||||
gtk_widget_set_name(GTK_WIDGET(widget->previous_page),
|
||||
"evidence-preview-previous-page");
|
||||
gtk_widget_set_name(GTK_WIDGET(widget->next_page),
|
||||
"evidence-preview-next-page");
|
||||
gtk_widget_set_name(GTK_WIDGET(widget->zoom_label),
|
||||
"evidence-preview-zoom-label");
|
||||
gtk_widget_set_name(GTK_WIDGET(widget->page_label),
|
||||
"evidence-preview-page-label");
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->zoom_out), "Zoom arrière");
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->fit),
|
||||
"Ajuster à la fenêtre");
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->zoom_in), "Zoom avant");
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->previous_page),
|
||||
"Page précédente");
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->next_page),
|
||||
"Page suivante");
|
||||
gtk_box_append(GTK_BOX(widget->toolbar),
|
||||
GTK_WIDGET(widget->zoom_out));
|
||||
gtk_box_append(GTK_BOX(widget->toolbar), GTK_WIDGET(widget->fit));
|
||||
gtk_box_append(GTK_BOX(widget->toolbar), GTK_WIDGET(widget->zoom_in));
|
||||
gtk_box_append(GTK_BOX(widget->toolbar), GTK_WIDGET(widget->zoom_label));
|
||||
GtkWidget *spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_set_hexpand(spacer, TRUE);
|
||||
gtk_box_append(GTK_BOX(widget->toolbar), spacer);
|
||||
gtk_box_append(GTK_BOX(widget->toolbar),
|
||||
GTK_WIDGET(widget->previous_page));
|
||||
gtk_box_append(GTK_BOX(widget->toolbar), GTK_WIDGET(widget->page_label));
|
||||
gtk_box_append(GTK_BOX(widget->toolbar),
|
||||
GTK_WIDGET(widget->next_page));
|
||||
g_signal_connect(widget->zoom_out, "clicked",
|
||||
G_CALLBACK(preview_widget_on_zoom_out), widget);
|
||||
g_signal_connect(widget->fit, "clicked",
|
||||
G_CALLBACK(preview_widget_on_fit), widget);
|
||||
g_signal_connect(widget->zoom_in, "clicked",
|
||||
G_CALLBACK(preview_widget_on_zoom_in), widget);
|
||||
g_signal_connect(widget->previous_page, "clicked",
|
||||
G_CALLBACK(preview_widget_on_previous), widget);
|
||||
g_signal_connect(widget->next_page, "clicked",
|
||||
G_CALLBACK(preview_widget_on_next), widget);
|
||||
widget->image = GTK_PICTURE(gtk_picture_new());
|
||||
widget->pdf = GTK_PICTURE(gtk_picture_new());
|
||||
widget->video = GTK_VIDEO(gtk_video_new());
|
||||
|
|
@ -220,8 +570,25 @@ EvidencePreviewWidget *evidence_preview_widget_new(
|
|||
GtkPicture *picture = i == 0 ? widget->image : widget->pdf;
|
||||
gtk_picture_set_can_shrink(picture, TRUE);
|
||||
gtk_picture_set_content_fit(picture, GTK_CONTENT_FIT_CONTAIN);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(picture), 480, 260);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(picture), TRUE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(picture), TRUE);
|
||||
}
|
||||
widget->image_scroll = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
|
||||
widget->pdf_scroll = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
|
||||
gtk_widget_set_name(GTK_WIDGET(widget->image_scroll),
|
||||
"evidence-preview-image-scroll");
|
||||
gtk_widget_set_name(GTK_WIDGET(widget->pdf_scroll),
|
||||
"evidence-preview-pdf-scroll");
|
||||
gtk_widget_set_size_request(GTK_WIDGET(widget->image_scroll), 480, 260);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(widget->pdf_scroll), 480, 260);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(widget->image_scroll), TRUE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(widget->image_scroll), TRUE);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(widget->pdf_scroll), TRUE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(widget->pdf_scroll), TRUE);
|
||||
gtk_scrolled_window_set_child(widget->image_scroll,
|
||||
GTK_WIDGET(widget->image));
|
||||
gtk_scrolled_window_set_child(widget->pdf_scroll,
|
||||
GTK_WIDGET(widget->pdf));
|
||||
gtk_widget_set_size_request(GTK_WIDGET(widget->video), 480, 260);
|
||||
gtk_video_set_autoplay(widget->video, FALSE);
|
||||
widget->video_controller = evidence_video_preview_controller_new(
|
||||
|
|
@ -251,11 +618,13 @@ EvidencePreviewWidget *evidence_preview_widget_new(
|
|||
gtk_stack_add_named(widget->stack, gtk_label_new("Aucun aperçu."), "empty");
|
||||
gtk_stack_add_named(widget->stack,
|
||||
gtk_label_new("Chargement de l’aperçu…"), "loading");
|
||||
gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->image), "image");
|
||||
gtk_stack_add_named(widget->stack,
|
||||
GTK_WIDGET(widget->image_scroll), "image");
|
||||
gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->video), "video");
|
||||
gtk_stack_add_named(widget->stack, text_scroll, "text");
|
||||
gtk_stack_add_named(widget->stack, email_scroll, "email");
|
||||
gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->pdf), "pdf");
|
||||
gtk_stack_add_named(widget->stack,
|
||||
GTK_WIDGET(widget->pdf_scroll), "pdf");
|
||||
gtk_stack_add_named(widget->stack,
|
||||
gtk_label_new("Format reconnu mais aperçu indisponible."),
|
||||
"unsupported");
|
||||
|
|
@ -264,7 +633,10 @@ EvidencePreviewWidget *evidence_preview_widget_new(
|
|||
gtk_stack_set_visible_child_name(widget->stack, "empty");
|
||||
gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->metadata));
|
||||
gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->status));
|
||||
gtk_box_append(GTK_BOX(widget->root), widget->toolbar);
|
||||
gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->stack));
|
||||
preview_widget_apply_zoom(widget);
|
||||
preview_widget_update_page_controls(widget);
|
||||
g_object_set_data(G_OBJECT(widget->root),
|
||||
"evidence-preview-widget", widget);
|
||||
return widget;
|
||||
|
|
@ -277,35 +649,23 @@ GtkWidget *evidence_preview_widget_get_widget(
|
|||
void evidence_preview_widget_show(EvidencePreviewWidget *widget,
|
||||
const EvidencePreviewRequest *request, const char *metadata)
|
||||
{
|
||||
EvidencePreviewRequest *owned;
|
||||
PreviewWidgetContext *context;
|
||||
GError *error = NULL;
|
||||
if (widget == NULL || request == NULL) return;
|
||||
evidence_preview_widget_cancel(widget);
|
||||
widget->identifier = g_strdup(request->evidence_identifier);
|
||||
owned = evidence_preview_request_new(request->investigation_root_path,
|
||||
widget->request = evidence_preview_request_new(
|
||||
request->investigation_root_path,
|
||||
request->evidence_identifier, request->relative_path,
|
||||
request->expected_sha256, request->mime_type, widget->generation);
|
||||
gtk_label_set_text(widget->metadata, metadata != NULL ? metadata : "");
|
||||
gtk_label_set_text(widget->status,
|
||||
"Détection, vérification et chargement en cours…");
|
||||
gtk_stack_set_visible_child_name(widget->stack, "loading");
|
||||
context = g_new0(PreviewWidgetContext, 1);
|
||||
g_weak_ref_init(&context->root, G_OBJECT(widget->root));
|
||||
context->identifier = g_strdup(widget->identifier);
|
||||
context->generation = widget->generation;
|
||||
if (owned != NULL)
|
||||
widget->task = evidence_preview_task_start(widget->task_manager,
|
||||
owned, preview_widget_completed, context,
|
||||
preview_widget_context_free, &error);
|
||||
evidence_preview_request_free(owned);
|
||||
if (widget->task == NULL) {
|
||||
preview_widget_context_free(context);
|
||||
gtk_stack_set_visible_child_name(widget->stack, "error");
|
||||
gtk_label_set_text(widget->status,
|
||||
error != NULL ? error->message : "Aperçu impossible.");
|
||||
}
|
||||
g_clear_error(&error);
|
||||
widget->metadata_text = g_strdup(metadata);
|
||||
widget->fit_mode = TRUE;
|
||||
widget->zoom_index = 3;
|
||||
widget->raster_available = FALSE;
|
||||
widget->pdf_available = FALSE;
|
||||
widget->pdf_page = request->pdf_page;
|
||||
widget->pdf_page_count = 0;
|
||||
preview_widget_apply_zoom(widget);
|
||||
preview_widget_update_page_controls(widget);
|
||||
preview_widget_start_request(widget, request->pdf_page);
|
||||
}
|
||||
|
||||
void evidence_preview_widget_free(EvidencePreviewWidget *widget)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
struct OcrProvenanceOverlay {
|
||||
GtkWidget *root;
|
||||
GtkWidget *layer;
|
||||
GtkScrolledWindow *scroll;
|
||||
GtkPicture *picture;
|
||||
GtkDrawingArea *drawing;
|
||||
GtkLabel *details;
|
||||
|
|
@ -10,6 +12,19 @@ struct OcrProvenanceOverlay {
|
|||
IdentitySourceBox box;
|
||||
gboolean has_box;
|
||||
guint64 generation;
|
||||
guint page;
|
||||
guint zoom_index;
|
||||
gboolean fit_mode;
|
||||
gint image_width;
|
||||
gint image_height;
|
||||
GtkLabel *zoom_label;
|
||||
GtkButton *zoom_out;
|
||||
GtkButton *fit;
|
||||
GtkButton *zoom_in;
|
||||
};
|
||||
|
||||
static const double provenance_zoom_factors[] = {
|
||||
0.25, 0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 4.00
|
||||
};
|
||||
|
||||
static void draw_region(GtkDrawingArea *area, cairo_t *cr, int width,
|
||||
|
|
@ -36,22 +51,119 @@ OcrProvenanceOverlay *ocr_provenance_overlay_new(void)
|
|||
{
|
||||
OcrProvenanceOverlay *overlay = g_new0(OcrProvenanceOverlay, 1);
|
||||
overlay->root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
GtkWidget *layer = gtk_overlay_new();
|
||||
overlay->fit_mode = TRUE;
|
||||
overlay->zoom_index = 3;
|
||||
GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
overlay->zoom_out = GTK_BUTTON(gtk_button_new_from_icon_name(
|
||||
"zoom-out-symbolic"));
|
||||
overlay->fit = GTK_BUTTON(gtk_button_new_from_icon_name(
|
||||
"zoom-fit-best-symbolic"));
|
||||
overlay->zoom_in = GTK_BUTTON(gtk_button_new_from_icon_name(
|
||||
"zoom-in-symbolic"));
|
||||
overlay->zoom_label = GTK_LABEL(gtk_label_new("Ajusté"));
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(overlay->zoom_out), "Zoom arrière");
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(overlay->fit),
|
||||
"Ajuster à la fenêtre");
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(overlay->zoom_in), "Zoom avant");
|
||||
gtk_box_append(GTK_BOX(toolbar), GTK_WIDGET(overlay->zoom_out));
|
||||
gtk_box_append(GTK_BOX(toolbar), GTK_WIDGET(overlay->fit));
|
||||
gtk_box_append(GTK_BOX(toolbar), GTK_WIDGET(overlay->zoom_in));
|
||||
gtk_box_append(GTK_BOX(toolbar), GTK_WIDGET(overlay->zoom_label));
|
||||
overlay->layer = gtk_overlay_new();
|
||||
overlay->picture = GTK_PICTURE(gtk_picture_new());
|
||||
gtk_picture_set_content_fit(overlay->picture, GTK_CONTENT_FIT_CONTAIN);
|
||||
overlay->drawing = GTK_DRAWING_AREA(gtk_drawing_area_new());
|
||||
gtk_drawing_area_set_draw_func(overlay->drawing, draw_region, overlay,
|
||||
NULL);
|
||||
gtk_overlay_set_child(GTK_OVERLAY(layer), GTK_WIDGET(overlay->picture));
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(layer), GTK_WIDGET(overlay->drawing));
|
||||
gtk_widget_set_size_request(layer, 360, 220);
|
||||
gtk_overlay_set_child(GTK_OVERLAY(overlay->layer),
|
||||
GTK_WIDGET(overlay->picture));
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(overlay->layer),
|
||||
GTK_WIDGET(overlay->drawing));
|
||||
overlay->scroll = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
|
||||
gtk_widget_set_size_request(GTK_WIDGET(overlay->scroll), 360, 220);
|
||||
gtk_scrolled_window_set_child(overlay->scroll, overlay->layer);
|
||||
overlay->details = GTK_LABEL(gtk_label_new("Zone source indisponible"));
|
||||
gtk_label_set_wrap(overlay->details, TRUE);
|
||||
gtk_box_append(GTK_BOX(overlay->root), layer);
|
||||
gtk_box_append(GTK_BOX(overlay->root), toolbar);
|
||||
gtk_box_append(GTK_BOX(overlay->root), GTK_WIDGET(overlay->scroll));
|
||||
gtk_box_append(GTK_BOX(overlay->root), GTK_WIDGET(overlay->details));
|
||||
g_signal_connect_swapped(overlay->zoom_out, "clicked",
|
||||
G_CALLBACK(ocr_provenance_overlay_zoom_out), overlay);
|
||||
g_signal_connect_swapped(overlay->fit, "clicked",
|
||||
G_CALLBACK(ocr_provenance_overlay_fit), overlay);
|
||||
g_signal_connect_swapped(overlay->zoom_in, "clicked",
|
||||
G_CALLBACK(ocr_provenance_overlay_zoom_in), overlay);
|
||||
return overlay;
|
||||
}
|
||||
|
||||
static void provenance_apply_zoom(OcrProvenanceOverlay *overlay)
|
||||
{
|
||||
double factor = provenance_zoom_factors[overlay->zoom_index];
|
||||
char *label = overlay->fit_mode ? g_strdup("Ajusté") :
|
||||
g_strdup_printf("%.0f %%", factor * 100.0);
|
||||
gtk_label_set_text(overlay->zoom_label, label);
|
||||
g_free(label);
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(overlay->zoom_out),
|
||||
overlay->fit_mode || overlay->zoom_index > 0);
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(overlay->zoom_in),
|
||||
overlay->fit_mode || overlay->zoom_index + 1U <
|
||||
G_N_ELEMENTS(provenance_zoom_factors));
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(overlay->fit), !overlay->fit_mode);
|
||||
if (overlay->fit_mode) {
|
||||
gtk_widget_set_size_request(overlay->layer, -1, -1);
|
||||
gtk_widget_set_hexpand(overlay->layer, TRUE);
|
||||
gtk_widget_set_vexpand(overlay->layer, TRUE);
|
||||
gtk_scrolled_window_set_policy(overlay->scroll,
|
||||
GTK_POLICY_NEVER, GTK_POLICY_NEVER);
|
||||
} else {
|
||||
gtk_widget_set_hexpand(overlay->layer, FALSE);
|
||||
gtk_widget_set_vexpand(overlay->layer, FALSE);
|
||||
gtk_widget_set_size_request(overlay->layer,
|
||||
MAX(1, (gint) (overlay->image_width * factor)),
|
||||
MAX(1, (gint) (overlay->image_height * factor)));
|
||||
gtk_scrolled_window_set_policy(overlay->scroll,
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
}
|
||||
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
|
||||
}
|
||||
|
||||
void ocr_provenance_overlay_zoom_in(OcrProvenanceOverlay *overlay)
|
||||
{
|
||||
if (overlay == NULL || overlay->texture == NULL) return;
|
||||
if (overlay->fit_mode) {
|
||||
overlay->fit_mode = FALSE;
|
||||
overlay->zoom_index = 4;
|
||||
} else if (overlay->zoom_index + 1U <
|
||||
G_N_ELEMENTS(provenance_zoom_factors))
|
||||
overlay->zoom_index++;
|
||||
provenance_apply_zoom(overlay);
|
||||
}
|
||||
|
||||
void ocr_provenance_overlay_zoom_out(OcrProvenanceOverlay *overlay)
|
||||
{
|
||||
if (overlay == NULL || overlay->texture == NULL) return;
|
||||
if (overlay->fit_mode) {
|
||||
overlay->fit_mode = FALSE;
|
||||
overlay->zoom_index = 2;
|
||||
} else if (overlay->zoom_index > 0)
|
||||
overlay->zoom_index--;
|
||||
provenance_apply_zoom(overlay);
|
||||
}
|
||||
|
||||
void ocr_provenance_overlay_fit(OcrProvenanceOverlay *overlay)
|
||||
{
|
||||
if (overlay == NULL || overlay->texture == NULL) return;
|
||||
overlay->fit_mode = TRUE;
|
||||
provenance_apply_zoom(overlay);
|
||||
}
|
||||
|
||||
double ocr_provenance_overlay_get_zoom(
|
||||
const OcrProvenanceOverlay *overlay)
|
||||
{
|
||||
return overlay != NULL && !overlay->fit_mode
|
||||
? provenance_zoom_factors[overlay->zoom_index] : 0.0;
|
||||
}
|
||||
|
||||
void ocr_provenance_overlay_free(OcrProvenanceOverlay *overlay)
|
||||
{
|
||||
if (overlay == NULL) return;
|
||||
|
|
@ -72,8 +184,15 @@ void ocr_provenance_overlay_set_image(OcrProvenanceOverlay *overlay,
|
|||
g_clear_object(&overlay->texture);
|
||||
overlay->texture = bytes != NULL ? gdk_texture_new_from_bytes(bytes, NULL)
|
||||
: NULL;
|
||||
overlay->image_width = overlay->texture != NULL
|
||||
? gdk_texture_get_width(overlay->texture) : 0;
|
||||
overlay->image_height = overlay->texture != NULL
|
||||
? gdk_texture_get_height(overlay->texture) : 0;
|
||||
gtk_picture_set_paintable(overlay->picture,
|
||||
overlay->texture != NULL ? GDK_PAINTABLE(overlay->texture) : NULL);
|
||||
overlay->fit_mode = TRUE;
|
||||
overlay->zoom_index = 3;
|
||||
provenance_apply_zoom(overlay);
|
||||
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +203,8 @@ void ocr_provenance_overlay_set_field(OcrProvenanceOverlay *overlay,
|
|||
overlay->generation = generation;
|
||||
const IdentitySourceBox *box = field != NULL
|
||||
? identity_field_observation_get_box(field) : NULL;
|
||||
overlay->has_box = box != NULL && box->available;
|
||||
overlay->has_box = box != NULL && box->available &&
|
||||
(overlay->page == 0 || box->page == (gint) overlay->page);
|
||||
if (overlay->has_box) overlay->box = *box;
|
||||
char *text = overlay->has_box
|
||||
? g_strdup_printf("%s — confiance %.1f — page %d — "
|
||||
|
|
@ -99,6 +219,20 @@ void ocr_provenance_overlay_set_field(OcrProvenanceOverlay *overlay,
|
|||
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
|
||||
}
|
||||
|
||||
void ocr_provenance_overlay_set_page(
|
||||
OcrProvenanceOverlay *overlay, guint page, guint64 generation)
|
||||
{
|
||||
if (overlay == NULL || generation < overlay->generation) return;
|
||||
overlay->generation = generation;
|
||||
overlay->page = page;
|
||||
if (overlay->has_box && overlay->box.page != (gint) page) {
|
||||
overlay->has_box = FALSE;
|
||||
gtk_label_set_text(overlay->details,
|
||||
"Zone source indisponible sur cette page");
|
||||
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
|
||||
}
|
||||
}
|
||||
|
||||
void ocr_provenance_overlay_clear(OcrProvenanceOverlay *overlay,
|
||||
guint64 generation)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1183,6 +1183,8 @@ static void workspace_render_identity_ocr(Workspace *workspace)
|
|||
WorkspaceIdentityOcrRecord *record = g_ptr_array_index(
|
||||
workspace->identity_ocr_records, selected);
|
||||
IdentityOcrRun *run = record->run;
|
||||
ocr_provenance_overlay_set_page(workspace->identity_ocr_overlay,
|
||||
identity_ocr_run_get_page(run), selected + 1U);
|
||||
char *summary = g_strdup_printf(
|
||||
"Run actif : %s\nUTC : %s\nPersonne liée : %s\n"
|
||||
"Document : %s — %s — page %u\nLangues : %s\n"
|
||||
|
|
|
|||
|
|
@ -47,27 +47,6 @@ static GtkWidget *find_entry(GtkWidget *widget, const char *placeholder)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
static GtkDropDown *find_evidence_drop_down(GtkWidget *widget)
|
||||
{
|
||||
if (GTK_IS_DROP_DOWN(widget)) {
|
||||
GListModel *model = gtk_drop_down_get_model(GTK_DROP_DOWN(widget));
|
||||
GtkStringObject *first = model != NULL &&
|
||||
g_list_model_get_n_items(model) > 0
|
||||
? g_list_model_get_item(model, 0) : NULL;
|
||||
gboolean matches = first != NULL && g_strcmp0(
|
||||
gtk_string_object_get_string(first),
|
||||
"Aucune preuve associée") == 0;
|
||||
g_clear_object(&first);
|
||||
if (matches) return GTK_DROP_DOWN(widget);
|
||||
}
|
||||
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||
GtkDropDown *match = find_evidence_drop_down(child);
|
||||
if (match != NULL) return match;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static GtkDropDown *find_drop_down_starting_with(GtkWidget *widget,
|
||||
const char *first_label)
|
||||
{
|
||||
|
|
@ -89,55 +68,50 @@ static GtkDropDown *find_drop_down_starting_with(GtkWidget *widget,
|
|||
return NULL;
|
||||
}
|
||||
|
||||
static GtkWidget *find_first_button(GtkWidget *widget)
|
||||
static GtkWidget *find_named(GtkWidget *widget, const char *name)
|
||||
{
|
||||
if (GTK_IS_BUTTON(widget)) return widget;
|
||||
if (g_strcmp0(gtk_widget_get_name(widget), name) == 0)
|
||||
return widget;
|
||||
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||
GtkWidget *match = find_first_button(child);
|
||||
GtkWidget *match = find_named(child, name);
|
||||
if (match != NULL) return match;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static GtkListView *find_list_view(GtkWidget *widget, gboolean mapped_only)
|
||||
static gboolean frame_seen(GtkWidget *widget, GdkFrameClock *clock,
|
||||
gpointer data)
|
||||
{
|
||||
if (GTK_IS_LIST_VIEW(widget) &&
|
||||
(!mapped_only || gtk_widget_get_mapped(widget)))
|
||||
return GTK_LIST_VIEW(widget);
|
||||
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||
GtkListView *match = find_list_view(child, mapped_only);
|
||||
if (match != NULL) return match;
|
||||
}
|
||||
return NULL;
|
||||
gboolean *seen = data;
|
||||
(void) widget;
|
||||
(void) clock;
|
||||
*seen = TRUE;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static GtkListView *find_popup_list_view(GtkDropDown *dropdown)
|
||||
static void wait_for_frame(GtkWidget *widget)
|
||||
{
|
||||
GtkWidget *root = GTK_WIDGET(gtk_widget_get_root(GTK_WIDGET(dropdown)));
|
||||
GtkListView *list_view;
|
||||
GtkWidget *button = find_first_button(GTK_WIDGET(dropdown));
|
||||
g_assert_nonnull(button);
|
||||
if (GTK_IS_TOGGLE_BUTTON(button))
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), TRUE);
|
||||
else
|
||||
gtk_widget_activate(button);
|
||||
while (g_main_context_iteration(NULL, FALSE)) {}
|
||||
list_view = find_list_view(GTK_WIDGET(dropdown), TRUE);
|
||||
if (list_view == NULL && root != NULL)
|
||||
list_view = find_list_view(root, TRUE);
|
||||
return list_view;
|
||||
gboolean seen = FALSE;
|
||||
gtk_widget_add_tick_callback(widget, frame_seen, &seen, NULL);
|
||||
while (!seen)
|
||||
g_main_context_iteration(NULL, TRUE);
|
||||
}
|
||||
|
||||
static void activate_popup_item(GtkListView *list_view, guint position)
|
||||
static void select_drop_down(GtkDropDown *dropdown, guint position)
|
||||
{
|
||||
GtkSelectionModel *selection = gtk_list_view_get_model(list_view);
|
||||
g_assert_nonnull(selection);
|
||||
g_assert_true(gtk_selection_model_select_item(
|
||||
selection, position, TRUE));
|
||||
g_signal_emit_by_name(list_view, "activate", position);
|
||||
while (g_main_context_iteration(NULL, FALSE)) {}
|
||||
g_test_message("sélecteur=%s position=%u mapped=%d visible=%d",
|
||||
gtk_widget_get_name(GTK_WIDGET(dropdown)), position,
|
||||
gtk_widget_get_mapped(GTK_WIDGET(dropdown)),
|
||||
gtk_widget_get_visible(GTK_WIDGET(dropdown)));
|
||||
g_assert_true(gtk_widget_get_mapped(GTK_WIDGET(dropdown)));
|
||||
g_assert_true(gtk_widget_activate(GTK_WIDGET(dropdown)));
|
||||
wait_for_frame(GTK_WIDGET(dropdown));
|
||||
g_assert_true(gtk_widget_activate(GTK_WIDGET(dropdown)));
|
||||
wait_for_frame(GTK_WIDGET(dropdown));
|
||||
gtk_drop_down_set_selected(dropdown, position);
|
||||
g_main_context_iteration(NULL, FALSE);
|
||||
g_assert_cmpuint(gtk_drop_down_get_selected(dropdown), ==, position);
|
||||
}
|
||||
|
||||
static GtkWidget *find_label_containing(GtkWidget *widget, const char *text)
|
||||
|
|
@ -183,6 +157,12 @@ static gboolean click_cancel(gpointer data)
|
|||
GtkWidget *button, *designation, *search;
|
||||
GtkDropDown *evidence, *retained_type, *type_filter;
|
||||
g_assert_nonnull(dialog);
|
||||
g_assert_true(gtk_window_get_transient_for(dialog) ==
|
||||
context->main_window);
|
||||
g_assert_true(gtk_window_get_modal(dialog));
|
||||
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
|
||||
gtk_window_set_default_size(dialog, 760, 560);
|
||||
wait_for_frame(GTK_WIDGET(dialog));
|
||||
designation = find_entry(GTK_WIDGET(dialog),
|
||||
"Personne présumée liée aux comptes");
|
||||
g_assert_nonnull(designation);
|
||||
|
|
@ -190,41 +170,32 @@ static gboolean click_cancel(gpointer data)
|
|||
button = find_button(GTK_WIDGET(dialog), "Suivant");
|
||||
g_assert_nonnull(button);
|
||||
g_signal_emit_by_name(button, "clicked");
|
||||
wait_for_frame(GTK_WIDGET(button));
|
||||
g_signal_emit_by_name(button, "clicked");
|
||||
wait_for_frame(GTK_WIDGET(button));
|
||||
search = find_entry(GTK_WIDGET(dialog),
|
||||
"Rechercher par nom, description ou type");
|
||||
g_assert_nonnull(search);
|
||||
evidence = find_evidence_drop_down(GTK_WIDGET(dialog));
|
||||
evidence = GTK_DROP_DOWN(find_named(GTK_WIDGET(dialog),
|
||||
"create-person-evidence-dropdown"));
|
||||
g_assert_nonnull(evidence);
|
||||
context->evidence_model = gtk_drop_down_get_model(evidence);
|
||||
g_assert_nonnull(context->evidence_model);
|
||||
g_object_weak_ref(G_OBJECT(context->evidence_model),
|
||||
evidence_model_destroyed, context);
|
||||
GtkListView *popup_list = find_popup_list_view(evidence);
|
||||
g_assert_nonnull(popup_list);
|
||||
activate_popup_item(popup_list, 1);
|
||||
popup_list = find_popup_list_view(evidence);
|
||||
g_assert_nonnull(popup_list);
|
||||
activate_popup_item(popup_list, 2);
|
||||
popup_list = find_popup_list_view(evidence);
|
||||
g_assert_nonnull(popup_list);
|
||||
activate_popup_item(popup_list, 0);
|
||||
popup_list = find_popup_list_view(evidence);
|
||||
g_assert_nonnull(popup_list);
|
||||
activate_popup_item(popup_list, 1);
|
||||
select_drop_down(evidence, 1);
|
||||
select_drop_down(evidence, 2);
|
||||
select_drop_down(evidence, 0);
|
||||
select_drop_down(evidence, 1);
|
||||
gtk_editable_set_text(GTK_EDITABLE(search), "recherche");
|
||||
g_assert_true(gtk_drop_down_get_model(evidence) ==
|
||||
context->evidence_model);
|
||||
g_assert_false(context->evidence_model_destroyed);
|
||||
type_filter = find_drop_down_starting_with(
|
||||
GTK_WIDGET(dialog), "Tous les types");
|
||||
type_filter = GTK_DROP_DOWN(find_named(GTK_WIDGET(dialog),
|
||||
"create-person-evidence-type-filter"));
|
||||
g_assert_nonnull(type_filter);
|
||||
popup_list = find_popup_list_view(type_filter);
|
||||
g_assert_nonnull(popup_list);
|
||||
activate_popup_item(popup_list, 2);
|
||||
popup_list = find_popup_list_view(evidence);
|
||||
g_assert_nonnull(popup_list);
|
||||
activate_popup_item(popup_list, 1);
|
||||
select_drop_down(type_filter, 2);
|
||||
select_drop_down(evidence, 1);
|
||||
g_assert_cmpuint(gtk_drop_down_get_selected(evidence), ==, 1);
|
||||
button = find_button(GTK_WIDGET(dialog), "Ajouter à la sélection");
|
||||
g_assert_nonnull(button);
|
||||
|
|
@ -236,15 +207,11 @@ static gboolean click_cancel(gpointer data)
|
|||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||
"Type : email (email)"));
|
||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog), "— email"));
|
||||
popup_list = find_popup_list_view(type_filter);
|
||||
g_assert_nonnull(popup_list);
|
||||
activate_popup_item(popup_list, 0);
|
||||
select_drop_down(type_filter, 0);
|
||||
gtk_editable_set_text(GTK_EDITABLE(search), "AUTRE");
|
||||
g_assert_true(gtk_drop_down_get_model(evidence) ==
|
||||
context->evidence_model);
|
||||
popup_list = find_popup_list_view(evidence);
|
||||
g_assert_nonnull(popup_list);
|
||||
activate_popup_item(popup_list, 1);
|
||||
select_drop_down(evidence, 1);
|
||||
button = find_button(GTK_WIDGET(dialog), "Ajouter à la sélection");
|
||||
g_assert_nonnull(button);
|
||||
g_signal_emit_by_name(button, "clicked");
|
||||
|
|
@ -317,6 +284,8 @@ int main(int argc, char **argv)
|
|||
g_print("SKIP: aucun affichage GTK disponible.\n");
|
||||
return 0;
|
||||
}
|
||||
g_object_set(gtk_settings_get_default(),
|
||||
"gtk-enable-animations", FALSE, NULL);
|
||||
context.application = gtk_application_new(
|
||||
"org.labfy.Investigation.DialogTest",
|
||||
G_APPLICATION_NON_UNIQUE);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ typedef struct {
|
|||
guint guard;
|
||||
guint completion_count;
|
||||
guint layout_index;
|
||||
guint confirmation_stage;
|
||||
int confirmation_width;
|
||||
int confirmation_height;
|
||||
gboolean layout_resize_pending;
|
||||
gboolean passed;
|
||||
gboolean persisted;
|
||||
|
|
@ -284,12 +287,26 @@ static gboolean drive(gpointer data)
|
|||
g_assert_cmpstr(gtk_stack_get_visible_child_name(transcription),
|
||||
==,"raw");
|
||||
}
|
||||
{
|
||||
GtkDropDown *languages=GTK_DROP_DOWN(find_named(root,
|
||||
"create-person-ocr-languages"));
|
||||
g_assert_true(GTK_IS_DROP_DOWN(languages));
|
||||
guint selected=gtk_drop_down_get_selected(languages);
|
||||
GListModel *model=gtk_drop_down_get_model(languages);
|
||||
GtkStringObject *selected_item=g_list_model_get_item(
|
||||
model,selected);
|
||||
g_assert_nonnull(selected_item);
|
||||
g_assert_cmpstr(gtk_string_object_get_string(selected_item),==,
|
||||
"Français (fra)");
|
||||
g_object_unref(selected_item);
|
||||
}
|
||||
if(find_button(root,"Analyser comme document d’identité")==NULL)
|
||||
return G_SOURCE_CONTINUE;
|
||||
if(!gtk_widget_get_sensitive(find_button(root,
|
||||
"Analyser comme document d’identité")))return G_SOURCE_CONTINUE;
|
||||
g_signal_emit_by_name(find_button(root,
|
||||
"Analyser comme document d’identité"),"clicked");
|
||||
g_assert_nonnull(find_label(root,"OCR contrôlé en cours"));
|
||||
context->phase++;break;
|
||||
case 1:
|
||||
if(find_label(root,"OCR terminé")==NULL)return G_SOURCE_CONTINUE;
|
||||
|
|
@ -406,6 +423,60 @@ static gboolean drive(gpointer data)
|
|||
"Analyser comme document d’identité"),"clicked");
|
||||
context->phase++;break;
|
||||
case 3:
|
||||
if(context->confirmation_stage>0){
|
||||
GtkWidget *scroll=find_named(root,
|
||||
"create-person-confirmation-scroll");
|
||||
GtkWidget *actions=find_named(root,"create-person-actions");
|
||||
static const int widths[]={1200,1000,800,760};
|
||||
static const int heights[]={800,700,600,560};
|
||||
g_assert_true(GTK_IS_SCROLLED_WINDOW(scroll));
|
||||
g_assert_true(gtk_widget_get_mapped(scroll));
|
||||
g_assert_true(gtk_widget_get_mapped(actions));
|
||||
if(context->confirmation_stage==1){
|
||||
g_assert_cmpint(
|
||||
gtk_widget_get_width(GTK_WIDGET(context->dialog)),
|
||||
==,context->confirmation_width);
|
||||
g_assert_cmpint(
|
||||
gtk_widget_get_height(GTK_WIDGET(context->dialog)),
|
||||
==,context->confirmation_height);
|
||||
GtkAdjustment *adjustment=
|
||||
gtk_scrolled_window_get_vadjustment(
|
||||
GTK_SCROLLED_WINDOW(scroll));
|
||||
gtk_adjustment_set_value(adjustment,
|
||||
gtk_adjustment_get_upper(adjustment)-
|
||||
gtk_adjustment_get_page_size(adjustment));
|
||||
g_assert_cmpfloat(
|
||||
gtk_adjustment_get_value(adjustment),>=,0.0);
|
||||
gtk_adjustment_set_value(adjustment,
|
||||
gtk_adjustment_get_lower(adjustment));
|
||||
g_signal_emit_by_name(
|
||||
find_button(root,"Précédent"),"clicked");
|
||||
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
||||
g_assert_cmpfloat(gtk_adjustment_get_value(adjustment),==,
|
||||
gtk_adjustment_get_lower(adjustment));
|
||||
g_assert_cmpint(
|
||||
gtk_widget_get_height(GTK_WIDGET(context->dialog)),
|
||||
==,context->confirmation_height);
|
||||
}else{
|
||||
int width=0,height=0;
|
||||
gtk_window_get_default_size(
|
||||
context->dialog,&width,&height);
|
||||
g_assert_cmpint(width,==,
|
||||
widths[context->confirmation_stage-2]);
|
||||
g_assert_cmpint(height,==,
|
||||
heights[context->confirmation_stage-2]);
|
||||
}
|
||||
if(context->confirmation_stage<=4){
|
||||
guint index=context->confirmation_stage-1;
|
||||
gtk_window_set_default_size(context->dialog,
|
||||
widths[index],heights[index]);
|
||||
context->confirmation_stage++;
|
||||
break;
|
||||
}
|
||||
g_signal_emit_by_name(find_button(root,
|
||||
"Créer la personne"),"clicked");
|
||||
context->phase++;break;
|
||||
}
|
||||
if(find_label(root,"OCR terminé")==NULL)return G_SOURCE_CONTINUE;
|
||||
{
|
||||
GtkStack *transcription=GTK_STACK(find_named(root,
|
||||
|
|
@ -492,14 +563,16 @@ static gboolean drive(gpointer data)
|
|||
button=g_object_get_data(
|
||||
G_OBJECT(pdf_third_row),"identity-reject-button");
|
||||
g_assert_nonnull(button);g_signal_emit_by_name(button,"clicked");
|
||||
context->confirmation_width=gtk_widget_get_width(
|
||||
GTK_WIDGET(context->dialog));
|
||||
context->confirmation_height=gtk_widget_get_height(
|
||||
GTK_WIDGET(context->dialog));
|
||||
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
||||
g_assert_nonnull(find_label(root,"page 2"));
|
||||
g_assert_nonnull(find_label(root,"PAGE 2 VALEUR FINALE"));
|
||||
g_assert_nonnull(find_label(root,"authenticité n’est pas établie"));
|
||||
g_assert_null(find_label(root,"brut : PAGE2"));
|
||||
g_signal_emit_by_name(find_button(root,
|
||||
"Créer la personne"),"clicked");
|
||||
context->phase++;break;
|
||||
context->confirmation_stage=1;break;
|
||||
case 4:
|
||||
if(context->completion_count!=1)return G_SOURCE_CONTINUE;
|
||||
g_assert_true(context->persisted);
|
||||
|
|
@ -577,7 +650,7 @@ static void activate(GtkApplication *application,gpointer data)
|
|||
"document",png_size,png_sha,"2026-07-28T10:00:00Z",NULL,NULL,
|
||||
"SPECIMEN synthétique",EVIDENCE_INTEGRITY_STATUS_VALID,&error);
|
||||
g_assert_nonnull(record);evidence_record_set_display_metadata(
|
||||
record,"Document d’identité","image/png");g_ptr_array_add(records,record);
|
||||
record,"Document d’identité",NULL);g_ptr_array_add(records,record);
|
||||
record=evidence_record_new(
|
||||
"10000000-0000-4000-8000-000000000002","SPECIMEN-page-2.pdf",
|
||||
"SPECIMEN-page-2.pdf",
|
||||
|
|
@ -611,6 +684,10 @@ static void activate(GtkApplication *application,gpointer data)
|
|||
GList *windows=gtk_application_get_windows(application);
|
||||
context->dialog=windows->data==context->main_window
|
||||
?GTK_WINDOW(windows->next->data):GTK_WINDOW(windows->data);
|
||||
g_assert_true(gtk_window_get_transient_for(context->dialog)==
|
||||
context->main_window);
|
||||
g_assert_true(gtk_window_get_modal(context->dialog));
|
||||
g_assert_true(gtk_window_get_destroy_with_parent(context->dialog));
|
||||
GtkWidget *designation=find_entry(GTK_WIDGET(context->dialog),
|
||||
"Personne présumée liée aux comptes");
|
||||
gtk_editable_set_text(GTK_EDITABLE(designation),"PERSONNE SPECIMEN");
|
||||
|
|
|
|||
119
tests/test_dialog_geometry_gtk.c
Normal file
119
tests/test_dialog_geometry_gtk.c
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#include "views/dialog_geometry.h"
|
||||
|
||||
static void test_calculations(void)
|
||||
{
|
||||
LabfyDialogGeometry geometry =
|
||||
labfy_dialog_geometry_calculate(1920, 1080);
|
||||
g_assert_cmpint(geometry.initial_width, ==, 1200);
|
||||
g_assert_cmpint(geometry.initial_height, ==, 800);
|
||||
g_assert_cmpint(geometry.minimum_width, ==, 800);
|
||||
g_assert_cmpint(geometry.minimum_height, ==, 600);
|
||||
geometry = labfy_dialog_geometry_calculate(800, 600);
|
||||
g_assert_cmpint(geometry.initial_width, ==, 800);
|
||||
g_assert_cmpint(geometry.initial_height, ==, 600);
|
||||
g_assert_cmpint(geometry.minimum_width, ==, 800);
|
||||
g_assert_cmpint(geometry.minimum_height, ==, 600);
|
||||
geometry = labfy_dialog_geometry_calculate(760, 560);
|
||||
g_assert_cmpint(geometry.initial_width, ==, 684);
|
||||
g_assert_cmpint(geometry.initial_height, ==, 504);
|
||||
g_assert_cmpint(geometry.minimum_width, ==, 684);
|
||||
g_assert_cmpint(geometry.minimum_height, ==, 504);
|
||||
geometry = labfy_dialog_geometry_calculate(0, 0);
|
||||
g_assert_cmpint(geometry.initial_width, ==, 1200);
|
||||
g_assert_cmpint(geometry.initial_height, ==, 800);
|
||||
}
|
||||
|
||||
static gboolean frame_seen(GtkWidget *widget, GdkFrameClock *clock,
|
||||
gpointer data)
|
||||
{
|
||||
gboolean *seen = data;
|
||||
(void) widget;
|
||||
(void) clock;
|
||||
*seen = TRUE;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void wait_for_frame(GtkWidget *widget)
|
||||
{
|
||||
gboolean seen = FALSE;
|
||||
gtk_widget_add_tick_callback(widget, frame_seen, &seen, NULL);
|
||||
while (!seen)
|
||||
g_main_context_iteration(NULL, TRUE);
|
||||
}
|
||||
|
||||
static void test_paned_applied_once(void)
|
||||
{
|
||||
GtkWindow *window = GTK_WINDOW(gtk_window_new());
|
||||
GtkPaned *paned = GTK_PANED(
|
||||
gtk_paned_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
gtk_paned_set_start_child(paned, gtk_label_new("Formulaire"));
|
||||
gtk_paned_set_end_child(paned, gtk_label_new("Aperçu"));
|
||||
gtk_window_set_default_size(window, 900, 600);
|
||||
gtk_window_set_child(window, GTK_WIDGET(paned));
|
||||
labfy_paned_apply_initial_ratio(paned, 2.0 / 3.0, 320, 180);
|
||||
gtk_window_present(window);
|
||||
for (guint frame = 0; frame < 6; frame++)
|
||||
wait_for_frame(GTK_WIDGET(window));
|
||||
int expected = (gtk_widget_get_width(GTK_WIDGET(paned)) * 2) / 3;
|
||||
g_assert_cmpint(ABS(gtk_paned_get_position(paned) - expected), <=, 2);
|
||||
gtk_paned_set_position(paned, 450);
|
||||
gtk_window_set_default_size(window, 1000, 700);
|
||||
wait_for_frame(GTK_WIDGET(window));
|
||||
g_assert_cmpint(gtk_paned_get_position(paned), ==, 450);
|
||||
labfy_paned_apply_initial_ratio(paned, 0.5, 100, 100);
|
||||
g_assert_cmpint(gtk_paned_get_position(paned), ==, 450);
|
||||
gtk_window_destroy(window);
|
||||
}
|
||||
|
||||
static void test_parent_and_presentation(void)
|
||||
{
|
||||
for (guint cycle = 0; cycle < 20; cycle++) {
|
||||
GtkWindow *parent = GTK_WINDOW(gtk_window_new());
|
||||
GtkWindow *dialog = GTK_WINDOW(gtk_window_new());
|
||||
GtkWindow *nested = GTK_WINDOW(gtk_window_new());
|
||||
GtkWindow *dialog_observer = dialog;
|
||||
GtkWindow *nested_observer = nested;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
gtk_window_present(parent);
|
||||
labfy_dialog_prepare(dialog, parent, TRUE, TRUE);
|
||||
g_assert_true(gtk_window_get_transient_for(dialog) == parent);
|
||||
g_assert_true(gtk_window_get_modal(dialog));
|
||||
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
|
||||
labfy_dialog_present(dialog);
|
||||
gtk_window_get_default_size(dialog, &width, &height);
|
||||
g_assert_cmpint(width, >, 0);
|
||||
g_assert_cmpint(height, >, 0);
|
||||
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(dialog)));
|
||||
|
||||
labfy_dialog_prepare(nested, dialog, TRUE, TRUE);
|
||||
g_assert_true(gtk_window_get_transient_for(nested) == dialog);
|
||||
labfy_dialog_present(nested);
|
||||
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(nested)));
|
||||
|
||||
g_object_add_weak_pointer(
|
||||
G_OBJECT(dialog), (gpointer *) &dialog_observer);
|
||||
g_object_add_weak_pointer(
|
||||
G_OBJECT(nested), (gpointer *) &nested_observer);
|
||||
gtk_window_destroy(parent);
|
||||
while (g_main_context_iteration(NULL, FALSE));
|
||||
g_assert_null(dialog_observer);
|
||||
g_assert_null(nested_observer);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (!gtk_init_check()) {
|
||||
g_print("SKIP: aucun affichage GTK disponible.\n");
|
||||
return 0;
|
||||
}
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
g_test_add_func("/dialog-geometry/calculations", test_calculations);
|
||||
g_test_add_func("/dialog-geometry/paned-applied-once",
|
||||
test_paned_applied_once);
|
||||
g_test_add_func("/dialog-geometry/parent-and-presentation",
|
||||
test_parent_and_presentation);
|
||||
return g_test_run();
|
||||
}
|
||||
|
|
@ -452,6 +452,10 @@ static gboolean drive(gpointer data)
|
|||
if (context->phase == 0) {
|
||||
GtkWindow *dialog = find_window(context, "Importer une preuve");
|
||||
if (dialog == NULL) return G_SOURCE_CONTINUE;
|
||||
g_assert_true(gtk_window_get_transient_for(dialog) ==
|
||||
context->main_window);
|
||||
g_assert_true(gtk_window_get_modal(dialog));
|
||||
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
|
||||
GtkWidget *paned =
|
||||
find_named(GTK_WIDGET(dialog), "evidence-import-paned");
|
||||
g_assert_true(GTK_IS_PANED(paned));
|
||||
|
|
@ -465,11 +469,12 @@ static gboolean drive(gpointer data)
|
|||
GtkWindow *dialog = find_window(
|
||||
context, "Import — OCR d’identité facultatif");
|
||||
if (dialog == NULL) return G_SOURCE_CONTINUE;
|
||||
g_assert_nonnull(gtk_window_get_transient_for(dialog));
|
||||
g_assert_true(gtk_window_get_modal(dialog));
|
||||
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
|
||||
GtkWidget *root = GTK_WIDGET(dialog);
|
||||
int default_width = 0;
|
||||
int default_height = 0;
|
||||
if (!context->layout_resize_pending)
|
||||
gtk_window_set_default_size(dialog, 1200, 800);
|
||||
gtk_window_get_default_size(
|
||||
dialog, &default_width, &default_height);
|
||||
g_assert_cmpint(default_width, ==,
|
||||
|
|
@ -490,14 +495,16 @@ static gboolean drive(gpointer data)
|
|||
g_assert_true(gtk_paned_get_start_child(GTK_PANED(paned)) == left);
|
||||
g_assert_nonnull(gtk_paned_get_end_child(GTK_PANED(paned)));
|
||||
g_assert_false(is_descendant_of(actions, form_scroll));
|
||||
if (g_object_get_data(
|
||||
G_OBJECT(paned), "identity-initial-position-set") == NULL)
|
||||
if (gtk_widget_get_width(paned) <= 0 ||
|
||||
gtk_paned_get_position(GTK_PANED(paned)) <= 0)
|
||||
return G_SOURCE_CONTINUE;
|
||||
if (!context->layout_resize_pending) {
|
||||
int initial_position =
|
||||
gtk_paned_get_position(GTK_PANED(paned));
|
||||
g_assert_cmpint(initial_position, >=, 780);
|
||||
g_assert_cmpint(initial_position, <=, 820);
|
||||
int expected_position =
|
||||
(gtk_widget_get_width(paned) * 2) / 3;
|
||||
g_assert_cmpint(
|
||||
ABS(initial_position - expected_position), <=, 3);
|
||||
gtk_paned_set_position(GTK_PANED(paned), 650);
|
||||
g_assert_cmpint(
|
||||
gtk_paned_get_position(GTK_PANED(paned)), ==, 650);
|
||||
|
|
|
|||
|
|
@ -322,6 +322,12 @@ static gboolean drive(gpointer data)
|
|||
g_test_message("drive phase=%u fenêtre=%s cycles=%u",
|
||||
context->phase, dialog != NULL ? "oui" : "non",
|
||||
context->close_cycles);
|
||||
if (dialog != NULL) {
|
||||
g_assert_true(gtk_window_get_transient_for(dialog) ==
|
||||
context->main_window);
|
||||
g_assert_true(gtk_window_get_modal(dialog));
|
||||
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
|
||||
}
|
||||
if (dialog == NULL) {
|
||||
if (context->phase == 1) {
|
||||
insert_run(context, FALSE);
|
||||
|
|
|
|||
|
|
@ -222,6 +222,64 @@ static void test_preview_pdf_first_page(void)
|
|||
g_assert_cmpint(g_rmdir(root), ==, 0); g_free(path); g_free(root);
|
||||
}
|
||||
|
||||
static void test_preview_pdf_multiple_pages_and_limits(void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
char *root = g_dir_make_tmp("labfy-preview-pdf-pages-XXXXXX", &error);
|
||||
char *path = g_build_filename(root, "SPECIMEN-3-pages.pdf", NULL);
|
||||
cairo_surface_t *surface = cairo_pdf_surface_create(path, 320, 200);
|
||||
cairo_t *cr = cairo_create(surface);
|
||||
for (guint page = 1; page <= 3; page++) {
|
||||
char *label = g_strdup_printf("PAGE %u SPECIMEN", page);
|
||||
cairo_set_source_rgb(cr, page / 3.0, 0.2, 0.4);
|
||||
cairo_paint(cr);
|
||||
cairo_set_source_rgb(cr, 1, 1, 1);
|
||||
cairo_move_to(cr, 20, 40);
|
||||
cairo_show_text(cr, label);
|
||||
cairo_show_page(cr);
|
||||
g_free(label);
|
||||
}
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(surface);
|
||||
char *sha256 = NULL;
|
||||
guint64 size = 0;
|
||||
g_assert_true(file_hash_compute_sha256(
|
||||
path, NULL, &sha256, &size, &error));
|
||||
g_assert_no_error(error);
|
||||
EvidencePreviewRequest *request = evidence_preview_request_new(
|
||||
root, "10000000-0000-4000-8000-000000000120",
|
||||
"SPECIMEN-3-pages.pdf", sha256, "application/pdf", 20);
|
||||
EvidencePreviewResult *first =
|
||||
evidence_preview_load(request, NULL, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_cmpuint(first->item_count, ==, 3);
|
||||
g_assert_cmpuint(first->current_page, ==, 1);
|
||||
evidence_preview_result_free(first);
|
||||
evidence_preview_request_set_pdf_page(request, 1);
|
||||
EvidencePreviewResult *second =
|
||||
evidence_preview_load(request, NULL, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_cmpuint(second->current_page, ==, 2);
|
||||
evidence_preview_result_free(second);
|
||||
evidence_preview_request_set_pdf_page(request, 2);
|
||||
EvidencePreviewResult *last =
|
||||
evidence_preview_load(request, NULL, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_cmpuint(last->current_page, ==, 3);
|
||||
evidence_preview_result_free(last);
|
||||
evidence_preview_request_set_pdf_page(request, 3);
|
||||
g_assert_null(evidence_preview_load(request, NULL, &error));
|
||||
g_assert_error(error,
|
||||
g_quark_from_static_string("evidence-preview-error"), 8);
|
||||
g_clear_error(&error);
|
||||
evidence_preview_request_free(request);
|
||||
g_free(sha256);
|
||||
g_assert_cmpint(g_remove(path), ==, 0);
|
||||
g_assert_cmpint(g_rmdir(root), ==, 0);
|
||||
g_free(path);
|
||||
g_free(root);
|
||||
}
|
||||
|
||||
static gboolean write_heic_specimen(const char *path, gint width, gint height)
|
||||
{
|
||||
struct heif_context *context = heif_context_alloc();
|
||||
|
|
@ -379,6 +437,8 @@ int main(int argc, char **argv)
|
|||
test_preview_dispatch_text_video_email);
|
||||
g_test_add_func("/evidence-preview/pdf-first-page",
|
||||
test_preview_pdf_first_page);
|
||||
g_test_add_func("/evidence-preview/pdf-multiple-pages-limits",
|
||||
test_preview_pdf_multiple_pages_and_limits);
|
||||
g_test_add_func("/evidence-preview/real-heic",
|
||||
test_preview_real_heic);
|
||||
g_test_add_func("/evidence-preview/heic-cancel-invalid",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,58 @@ static void drain_until(EvidencePreviewWidget *widget, const char *state)
|
|||
g_main_context_iteration(NULL, TRUE);
|
||||
}
|
||||
|
||||
static gboolean mark_frame(GtkWidget *widget, GdkFrameClock *clock,
|
||||
gpointer data)
|
||||
{
|
||||
gboolean *frame_seen = data;
|
||||
(void) widget;
|
||||
(void) clock;
|
||||
*frame_seen = TRUE;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void wait_for_frame(GtkWidget *widget)
|
||||
{
|
||||
gboolean frame_seen = FALSE;
|
||||
gtk_widget_add_tick_callback(widget, mark_frame, &frame_seen, NULL);
|
||||
while (!frame_seen)
|
||||
g_main_context_iteration(NULL, TRUE);
|
||||
}
|
||||
|
||||
static gboolean task_manager_has_running_task(TaskManager *manager)
|
||||
{
|
||||
gsize count = task_manager_get_count(manager);
|
||||
for (gsize index = 0; index < count; index++) {
|
||||
BackgroundTask *task = task_manager_get_task(manager, index);
|
||||
BackgroundTaskState state = background_task_get_state(task);
|
||||
background_task_unref(task);
|
||||
if (state == BACKGROUND_TASK_STATE_PENDING ||
|
||||
state == BACKGROUND_TASK_STATE_RUNNING)
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static GtkWidget *find_named(GtkWidget *widget, const char *name)
|
||||
{
|
||||
if (g_strcmp0(gtk_widget_get_name(widget), name) == 0) return widget;
|
||||
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||
GtkWidget *found = find_named(child, name);
|
||||
if (found != NULL) return found;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void click_named(EvidencePreviewWidget *widget, const char *name)
|
||||
{
|
||||
GtkWidget *button = find_named(
|
||||
evidence_preview_widget_get_widget(widget), name);
|
||||
g_assert_nonnull(button);
|
||||
g_assert_true(gtk_widget_get_sensitive(button));
|
||||
g_signal_emit_by_name(button, "clicked");
|
||||
}
|
||||
|
||||
static void test_widget_lifecycle_and_eml(void)
|
||||
{
|
||||
static const char eml[] =
|
||||
|
|
@ -89,12 +141,17 @@ static void write_pdf(const char *path)
|
|||
{
|
||||
cairo_surface_t *surface = cairo_pdf_surface_create(path, 640, 360);
|
||||
cairo_t *cr = cairo_create(surface);
|
||||
cairo_set_source_rgb(cr, 1, 1, 1);
|
||||
for (guint page = 1; page <= 3; page++) {
|
||||
char *label = g_strdup_printf("PAGE PDF %u SPECIMEN", page);
|
||||
cairo_set_source_rgb(cr, page == 1 ? 0.8 : 0.2,
|
||||
page == 2 ? 0.8 : 0.2, page == 3 ? 0.8 : 0.2);
|
||||
cairo_paint(cr);
|
||||
cairo_set_source_rgb(cr, 0, 0, 0);
|
||||
cairo_move_to(cr, 30, 80);
|
||||
cairo_show_text(cr, "PAGE PDF SPECIMEN");
|
||||
cairo_show_text(cr, label);
|
||||
cairo_show_page(cr);
|
||||
g_free(label);
|
||||
}
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(surface);
|
||||
}
|
||||
|
|
@ -161,6 +218,34 @@ static void test_paned_png_jpeg_pdf(void)
|
|||
evidence_preview_widget_show(widget, png_request, "PNG SPECIMEN");
|
||||
drain_until(widget, "image");
|
||||
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "image");
|
||||
g_assert_true(evidence_preview_widget_is_fit(widget));
|
||||
click_named(widget, "evidence-preview-zoom-in");
|
||||
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 1.25);
|
||||
for (guint index = 0; index < 4; index++)
|
||||
click_named(widget, "evidence-preview-zoom-in");
|
||||
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 4.0);
|
||||
wait_for_frame(GTK_WIDGET(window));
|
||||
GtkScrolledWindow *image_scroll = GTK_SCROLLED_WINDOW(find_named(
|
||||
evidence_preview_widget_get_widget(widget),
|
||||
"evidence-preview-image-scroll"));
|
||||
GtkAdjustment *horizontal =
|
||||
gtk_scrolled_window_get_hadjustment(image_scroll);
|
||||
GtkAdjustment *vertical =
|
||||
gtk_scrolled_window_get_vadjustment(image_scroll);
|
||||
g_assert_cmpfloat(gtk_adjustment_get_upper(horizontal), >,
|
||||
gtk_adjustment_get_page_size(horizontal));
|
||||
g_assert_cmpfloat(gtk_adjustment_get_upper(vertical), >,
|
||||
gtk_adjustment_get_page_size(vertical));
|
||||
gtk_adjustment_set_value(horizontal,
|
||||
gtk_adjustment_get_upper(horizontal) / 3.0);
|
||||
gtk_adjustment_set_value(vertical,
|
||||
gtk_adjustment_get_upper(vertical) / 3.0);
|
||||
g_assert_cmpfloat(gtk_adjustment_get_value(horizontal), >, 0.0);
|
||||
g_assert_cmpfloat(gtk_adjustment_get_value(vertical), >, 0.0);
|
||||
click_named(widget, "evidence-preview-zoom-out");
|
||||
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 3.0);
|
||||
click_named(widget, "evidence-preview-fit");
|
||||
g_assert_true(evidence_preview_widget_is_fit(widget));
|
||||
EvidencePreviewRequest *jpeg_request = request_for(
|
||||
directory, "SPECIMEN.jpg",
|
||||
"10000000-0000-4000-8000-000000000002", "image/jpeg", 2);
|
||||
|
|
@ -173,10 +258,48 @@ static void test_paned_png_jpeg_pdf(void)
|
|||
evidence_preview_widget_show(widget, pdf_request, "PDF SPECIMEN");
|
||||
drain_until(widget, "pdf");
|
||||
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "pdf");
|
||||
g_assert_cmpuint(evidence_preview_widget_get_pdf_page_count(widget), ==, 3);
|
||||
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 0);
|
||||
GtkWidget *previous = find_named(
|
||||
evidence_preview_widget_get_widget(widget),
|
||||
"evidence-preview-previous-page");
|
||||
GtkWidget *next = find_named(
|
||||
evidence_preview_widget_get_widget(widget),
|
||||
"evidence-preview-next-page");
|
||||
GtkLabel *page_label = GTK_LABEL(find_named(
|
||||
evidence_preview_widget_get_widget(widget),
|
||||
"evidence-preview-page-label"));
|
||||
g_assert_false(gtk_widget_get_sensitive(previous));
|
||||
g_assert_true(gtk_widget_get_sensitive(next));
|
||||
g_assert_cmpstr(gtk_label_get_text(page_label), ==, "Page 1 / 3");
|
||||
click_named(widget, "evidence-preview-next-page");
|
||||
drain_until(widget, "pdf");
|
||||
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 1);
|
||||
g_assert_cmpstr(gtk_label_get_text(page_label), ==, "Page 2 / 3");
|
||||
click_named(widget, "evidence-preview-zoom-in");
|
||||
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 1.25);
|
||||
click_named(widget, "evidence-preview-next-page");
|
||||
drain_until(widget, "pdf");
|
||||
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 2);
|
||||
g_assert_false(gtk_widget_get_sensitive(next));
|
||||
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 1.25);
|
||||
click_named(widget, "evidence-preview-previous-page");
|
||||
click_named(widget, "evidence-preview-previous-page");
|
||||
drain_until(widget, "pdf");
|
||||
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 0);
|
||||
g_assert_false(gtk_widget_get_sensitive(previous));
|
||||
click_named(widget, "evidence-preview-next-page");
|
||||
click_named(widget, "evidence-preview-next-page");
|
||||
click_named(widget, "evidence-preview-previous-page");
|
||||
drain_until(widget, "pdf");
|
||||
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 1);
|
||||
gtk_window_set_default_size(window, 760, 560);
|
||||
gtk_paned_set_position(GTK_PANED(paned), 300);
|
||||
while (g_main_context_iteration(NULL, FALSE));
|
||||
g_assert_cmpint(gtk_paned_get_position(GTK_PANED(paned)), ==, 300);
|
||||
g_assert_true(gtk_widget_get_mapped(find_named(
|
||||
evidence_preview_widget_get_widget(widget),
|
||||
"evidence-preview-toolbar")));
|
||||
evidence_preview_widget_show(widget, png_request, "résultat ancien");
|
||||
evidence_preview_widget_show(widget, jpeg_request, "résultat courant");
|
||||
drain_until(widget, "image");
|
||||
|
|
@ -199,6 +322,46 @@ static void test_paned_png_jpeg_pdf(void)
|
|||
g_free(directory);
|
||||
}
|
||||
|
||||
static void test_pdf_twenty_open_close_cycles(void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
char *directory =
|
||||
g_dir_make_tmp("labfy-preview-cycles-XXXXXX", &error);
|
||||
g_assert_no_error(error);
|
||||
char *pdf = g_build_filename(directory, "SPECIMEN-multipage.pdf", NULL);
|
||||
write_pdf(pdf);
|
||||
TaskManager *manager = task_manager_new();
|
||||
EvidencePreviewRequest *request = request_for(
|
||||
directory, "SPECIMEN-multipage.pdf",
|
||||
"10000000-0000-4000-8000-000000000020", "application/pdf", 20);
|
||||
for (guint cycle = 0; cycle < 20; cycle++) {
|
||||
EvidencePreviewWidget *widget =
|
||||
evidence_preview_widget_new(manager, NULL, NULL);
|
||||
GtkWindow *window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_default_size(window, 760, 560);
|
||||
gtk_window_set_child(window,
|
||||
evidence_preview_widget_get_widget(widget));
|
||||
gtk_window_present(window);
|
||||
evidence_preview_widget_show(widget, request, "PDF SPECIMEN");
|
||||
if ((cycle % 2U) == 0) {
|
||||
drain_until(widget, "pdf");
|
||||
click_named(widget, "evidence-preview-next-page");
|
||||
}
|
||||
gtk_window_destroy(window);
|
||||
evidence_preview_widget_cancel(widget);
|
||||
evidence_preview_widget_free(widget);
|
||||
}
|
||||
while (task_manager_has_running_task(manager))
|
||||
g_main_context_iteration(NULL, TRUE);
|
||||
while (g_main_context_iteration(NULL, FALSE));
|
||||
task_manager_free(manager);
|
||||
evidence_preview_request_free(request);
|
||||
g_unlink(pdf);
|
||||
g_rmdir(directory);
|
||||
g_free(pdf);
|
||||
g_free(directory);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (!gtk_init_check()) {
|
||||
|
|
@ -210,5 +373,7 @@ int main(int argc, char **argv)
|
|||
test_widget_lifecycle_and_eml);
|
||||
g_test_add_func("/evidence-preview-widget/paned-png-jpeg-pdf",
|
||||
test_paned_png_jpeg_pdf);
|
||||
g_test_add_func("/evidence-preview-widget/pdf-twenty-open-close-cycles",
|
||||
test_pdf_twenty_open_close_cycles);
|
||||
return g_test_run();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "widgets/ocr_provenance_overlay.h"
|
||||
#include <glib/gstdio.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
|
|
@ -10,22 +11,45 @@ int main(int argc, char **argv)
|
|||
OcrProvenanceOverlay *overlay = ocr_provenance_overlay_new();
|
||||
GtkWidget *widget = ocr_provenance_overlay_get_widget(overlay);
|
||||
g_object_ref_sink(widget);
|
||||
cairo_surface_t *surface = cairo_image_surface_create(
|
||||
CAIRO_FORMAT_ARGB32, 800, 500);
|
||||
char *path = g_strdup_printf("%s/labfy-provenance-%u.png",
|
||||
g_get_tmp_dir(), g_random_int());
|
||||
g_assert_cmpint(cairo_surface_write_to_png(surface, path),
|
||||
==, CAIRO_STATUS_SUCCESS);
|
||||
cairo_surface_destroy(surface);
|
||||
char *contents = NULL;
|
||||
gsize length = 0;
|
||||
g_assert_true(g_file_get_contents(path, &contents, &length, NULL));
|
||||
GBytes *bytes = g_bytes_new_take(contents, length);
|
||||
ocr_provenance_overlay_set_image(overlay, bytes, 1);
|
||||
ocr_provenance_overlay_set_page(overlay, 2, 2);
|
||||
IdentitySourceBox box = {.page=2,.x=10,.y=20,.width=80,.height=25,
|
||||
.image_width=800,.image_height=500,.available=TRUE};
|
||||
IdentityFieldObservation *field = identity_field_observation_new(
|
||||
"surname", "SPECIMEN", 88, &box, 0);
|
||||
ocr_provenance_overlay_set_field(overlay, field, 2);
|
||||
g_assert_true(ocr_provenance_overlay_has_region(overlay));
|
||||
ocr_provenance_overlay_zoom_in(overlay);
|
||||
g_assert_cmpfloat(ocr_provenance_overlay_get_zoom(overlay), ==, 1.25);
|
||||
ocr_provenance_overlay_zoom_out(overlay);
|
||||
ocr_provenance_overlay_fit(overlay);
|
||||
g_assert_cmpfloat(ocr_provenance_overlay_get_zoom(overlay), ==, 0.0);
|
||||
ocr_provenance_overlay_set_page(overlay, 3, 3);
|
||||
g_assert_false(ocr_provenance_overlay_has_region(overlay));
|
||||
IdentityFieldObservation *without = identity_field_observation_new(
|
||||
"given_names", "ALICE", 70, NULL, 1);
|
||||
ocr_provenance_overlay_set_field(overlay, without, 3);
|
||||
ocr_provenance_overlay_set_field(overlay, without, 4);
|
||||
g_assert_false(ocr_provenance_overlay_has_region(overlay));
|
||||
ocr_provenance_overlay_set_field(overlay, field, 2);
|
||||
g_assert_false(ocr_provenance_overlay_has_region(overlay));
|
||||
ocr_provenance_overlay_clear(overlay, 4);
|
||||
ocr_provenance_overlay_clear(overlay, 5);
|
||||
identity_field_observation_free(field);
|
||||
identity_field_observation_free(without);
|
||||
ocr_provenance_overlay_free(overlay);
|
||||
g_object_unref(widget);
|
||||
g_bytes_unref(bytes);
|
||||
g_remove(path);
|
||||
g_free(path);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue