feat(android): copy from the list, framed entries, one radius everywhere

The cross moves inside the search field, where it belonged. My first attempt failed with a TLabel, whose HitTest is false by default, and I concluded too quickly that no child of a TEdit can take a touch. A TRectangle takes it.

Buttons drop the pill and take the fields' radius: a stadium beside a 14-radius field put two shape vocabularies on one line, and consistency is worth more here than fidelity to the reference.

List entries get their own block, like the detail fields - on a gradient a bare line of text has no readable edge. And each row carries User and Pass buttons, so the commonest gesture no longer requires opening the entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-16 22:44:52 +01:00
parent 218a0dde03
commit e85f46f501
2 changed files with 84 additions and 58 deletions
+9 -19
View File
@@ -43,6 +43,10 @@ const
COL_MUTED = TAlphaColor($B3E8F0FA); COL_MUTED = TAlphaColor($B3E8F0FA);
COL_ON_ACC = TAlphaColor($FFFFFFFF); // le blanc reste lisible sur 30 % COL_ON_ACC = TAlphaColor($FFFFFFFF); // le blanc reste lisible sur 30 %
/// Un seul arrondi pour tout ce qui se touche - champs comme boutons. Il
/// doit suivre celui de make-style.ps1, qui l'applique aux champs natifs.
RADIUS = 14;
/// A appeler une fois, avant la creation de la forme. /// A appeler une fois, avant la creation de la forme.
procedure ApplyDarkTheme; procedure ApplyDarkTheme;
@@ -105,10 +109,6 @@ type
procedure Up(Sender: TObject; Button: TMouseButton; Shift: TShiftState; procedure Up(Sender: TObject; Button: TMouseButton; Shift: TShiftState;
X, Y: Single); X, Y: Single);
procedure Leave(Sender: TObject); procedure Leave(Sender: TObject);
/// L'arrondi se recalcule a chaque changement de taille : la hauteur est
/// posee APRES la creation, et un arrondi fixe superieur a la moitie de
/// celle-ci transforme le bouton en ellipse.
procedure Resized(Sender: TObject);
end; end;
procedure TPressFeedback.Down(Sender: TObject; Button: TMouseButton; procedure TPressFeedback.Down(Sender: TObject; Button: TMouseButton;
@@ -128,16 +128,6 @@ begin
if Sender is TRectangle then TRectangle(Sender).Opacity := 1; if Sender is TRectangle then TRectangle(Sender).Opacity := 1;
end; end;
procedure TPressFeedback.Resized(Sender: TObject);
var
LR: TRectangle;
begin
if not (Sender is TRectangle) then Exit;
LR := TRectangle(Sender);
LR.XRadius := LR.Height / 2;
LR.YRadius := LR.Height / 2;
end;
function Surface(AParent: TFmxObject): TRectangle; function Surface(AParent: TFmxObject): TRectangle;
begin begin
Result := TRectangle.Create(AParent); Result := TRectangle.Create(AParent);
@@ -178,9 +168,11 @@ var
begin begin
Result := TRectangle.Create(AParent); Result := TRectangle.Create(AParent);
Result.Parent := AParent; Result.Parent := AParent;
// Pilule : l'arrondi vaut exactement la moitie de la hauteur, et se // MEME ARRONDI QUE LES CHAMPS. Un bouton en pilule a cote d'un champ a 14
// recalcule quand elle change (voir TPressFeedback.Resized). Tout ce qui se // donnait deux vocabulaires de forme sur la meme ligne ; la coherence vaut
// touche est arrondi ainsi dans la reference. // mieux que la fidelite a la reference sur ce point.
Result.XRadius := RADIUS;
Result.YRadius := RADIUS;
Result.Cursor := crHandPoint; Result.Cursor := crHandPoint;
Result.Stroke.Kind := TBrushKind.None; Result.Stroke.Kind := TBrushKind.None;
// La difference entre principal et secondaire tient a l'opacite du blanc, // La difference entre principal et secondaire tient a l'opacite du blanc,
@@ -208,8 +200,6 @@ begin
Result.OnMouseDown := LFeedback.Down; Result.OnMouseDown := LFeedback.Down;
Result.OnMouseUp := LFeedback.Up; Result.OnMouseUp := LFeedback.Up;
Result.OnMouseLeave := LFeedback.Leave; Result.OnMouseLeave := LFeedback.Leave;
Result.OnResize := LFeedback.Resized;
LFeedback.Resized(Result);
end; end;
procedure SetCornerRadius(AButton: TRectangle; AValue: Single); procedure SetCornerRadius(AButton: TRectangle; AValue: Single);
+75 -39
View File
@@ -185,6 +185,8 @@ type
procedure OnSearchChanged(Sender: TObject); procedure OnSearchChanged(Sender: TObject);
procedure OnSearchClear(Sender: TObject); procedure OnSearchClear(Sender: TObject);
procedure OnRowCopyUser(Sender: TObject);
procedure OnRowCopyPwd(Sender: TObject);
procedure OnFolderTap(Sender: TObject); procedure OnFolderTap(Sender: TObject);
procedure OnFolderPick(Sender: TObject); procedure OnFolderPick(Sender: TObject);
procedure PopulateFolders; procedure PopulateFolders;
@@ -395,9 +397,8 @@ end;
procedure TForm8.BuildList; procedure TForm8.BuildList;
var var
LBar, LTop, LSearchWrap: TLayout; LBar, LTop: TLayout;
LBtnLock: TRectangle; LBtnLock: TRectangle;
LClearMark: TLabel;
LGlass: TCircle; LGlass: TCircle;
LHandle: TLine; LHandle: TLine;
begin begin
@@ -452,41 +453,11 @@ begin
FBtnFolder.Width := 170; FBtnFolder.Width := 170;
FBtnFolder.Margins.Left := 8; FBtnFolder.Margins.Left := 8;
// La croix vit A COTE du champ, pas dedans. Un enfant place dans un TEdit
// ne recoit pas les touches de facon fiable : le controle natif les capte
// sur toute sa surface. Un conteneur avec un Right et un Client leve en
// plus toute question d'ordre.
LSearchWrap := TLayout.Create(Self);
LSearchWrap.Parent := LTop;
LSearchWrap.Align := TAlignLayout.Client;
// La place de la croix est RESERVEE en permanence : la montrer et la cacher
// faisait changer la largeur du champ a chaque frappe, et la ligne sautait.
// Seule la croix elle-meme apparait et disparait.
FSearchClear := TRectangle.Create(Self);
FSearchClear.Parent := LSearchWrap;
FSearchClear.Align := TAlignLayout.Right;
FSearchClear.Width := 46;
FSearchClear.Fill.Kind := TBrushKind.None; // invisible, mais cliquable
FSearchClear.Stroke.Kind := TBrushKind.None;
FSearchClear.OnClick := OnSearchClear;
LClearMark := TLabel.Create(FSearchClear);
LClearMark.Parent := FSearchClear;
LClearMark.Align := TAlignLayout.Client;
LClearMark.HitTest := False; // le clic va au rectangle
LClearMark.Text := 'X';
LClearMark.TextSettings.HorzAlign := TTextAlign.Center;
LClearMark.TextSettings.VertAlign := TTextAlign.Center;
LClearMark.Visible := False;
Tint(LClearMark, 17, True);
FSearchClearMark := LClearMark;
// Le champ reste natif - il faut un clavier - et son fond vient desormais du // Le champ reste natif - il faut un clavier - et son fond vient desormais du
// style, recolore a la palette par make-style.ps1. PAS de rectangle derriere // style, recolore a la palette par make-style.ps1. PAS de rectangle derriere
// lui : il en resultait deux cadres arrondis emboites. // lui : il en resultait deux cadres arrondis emboites.
FSearch := TEdit.Create(Self); FSearch := TEdit.Create(Self);
FSearch.Parent := LSearchWrap; FSearch.Parent := LTop;
FSearch.Align := TAlignLayout.Client; FSearch.Align := TAlignLayout.Client;
// Le texte saisi commence apres la loupe. // Le texte saisi commence apres la loupe.
FSearch.Padding.Left := 32; FSearch.Padding.Left := 32;
@@ -527,6 +498,29 @@ begin
FSearchHint.Opacity := 0.45; FSearchHint.Opacity := 0.45;
FSearchHint.Text := 'Search'; FSearchHint.Text := 'Search';
// Croix DANS le champ, et non plus a cote. Le premier essai avait echoue
// avec un TLabel, dont HitTest vaut faux par defaut ; j'en avais conclu trop
// vite qu'un enfant de TEdit ne recoit jamais les touches. Un TRectangle les
// prend. Sa place est occupee en permanence pour que la largeur du champ ne
// bouge pas ; seule la croix apparait.
FSearchClear := TRectangle.Create(FSearch);
FSearchClear.Parent := FSearch;
FSearchClear.Align := TAlignLayout.Right;
FSearchClear.Width := 46;
FSearchClear.Fill.Kind := TBrushKind.None; // invisible, mais cliquable
FSearchClear.Stroke.Kind := TBrushKind.None;
FSearchClear.OnClick := OnSearchClear;
FSearchClearMark := TLabel.Create(FSearchClear);
FSearchClearMark.Parent := FSearchClear;
FSearchClearMark.Align := TAlignLayout.Client;
FSearchClearMark.HitTest := False; // le clic va au rectangle
FSearchClearMark.Text := 'X';
FSearchClearMark.TextSettings.HorzAlign := TTextAlign.Center;
FSearchClearMark.TextSettings.VertAlign := TTextAlign.Center;
FSearchClearMark.Visible := False;
Tint(FSearchClearMark, 17, True);
// Croix d'effacement, montree seulement quand il y a quelque chose a // Croix d'effacement, montree seulement quand il y a quelque chose a
// effacer. Un TLabel et non un bouton : dans un champ, une croix se touche, // effacer. Un TLabel et non un bouton : dans un champ, une croix se touche,
// elle ne se dessine pas comme une commande. // elle ne se dessine pas comme une commande.
@@ -1363,6 +1357,22 @@ begin
SetButtonText(FBtnFolder, FFolderNames[FFolderSel]); SetButtonText(FBtnFolder, FFolderNames[FFolderSel]);
end; end;
/// Copie depuis la liste. Le Tag du bouton porte l'indice dans le coffre, pas
/// dans la liste affichee - la recherche renumerote les lignes, pas le coffre.
procedure TForm8.OnRowCopyUser(Sender: TObject);
begin
Touch;
if (Sender is TRectangle) and (TRectangle(Sender).Tag < FVault.Count) then
CopyToClipboard(FVault.Entry(TRectangle(Sender).Tag).Username, 'Username');
end;
procedure TForm8.OnRowCopyPwd(Sender: TObject);
begin
Touch;
if (Sender is TRectangle) and (TRectangle(Sender).Tag < FVault.Count) then
CopyToClipboard(FVault.Entry(TRectangle(Sender).Tag).Password, 'Password');
end;
procedure TForm8.OnSearchClear(Sender: TObject); procedure TForm8.OnSearchClear(Sender: TObject);
begin begin
FSearch.Text := ''; FSearch.Text := '';
@@ -1441,7 +1451,8 @@ var
LFolder: string; LFolder: string;
LBmp: TBitmap; LBmp: TBitmap;
LImg: TImage; LImg: TImage;
LBody: TLayout; LBody, LActions: TLayout;
LCard, LBtnUser, LBtnPwd: TRectangle;
LTitle, LSub: TLabel; LTitle, LSub: TLabel;
begin begin
// ponytail: refonte complete de la liste a chaque frappe. Sur un coffre de // ponytail: refonte complete de la liste a chaque frappe. Sur un coffre de
@@ -1467,8 +1478,33 @@ begin
E := FVault.Entry(FFiltered[I]); E := FVault.Entry(FFiltered[I]);
LItem := TListBoxItem.Create(FList); LItem := TListBoxItem.Create(FList);
LItem.Parent := FList; LItem.Parent := FList;
LItem.Height := 56; LItem.Height := 76;
LItem.Tag := FFiltered[I]; // indice dans le coffre, pas dans la liste LItem.Tag := FFiltered[I]; // indice dans le coffre, pas dans la liste
// Chaque entree dans son bloc, comme les champs du detail : sur un fond
// en degrade, une simple ligne de texte flotte sans limite lisible.
LCard := Surface(LItem);
LCard.Align := TAlignLayout.Client;
LCard.Margins.Rect := RectF(4, 3, 4, 3);
// Copier sans ouvrir l'entree - le geste le plus frequent. Les deux
// boutons sont poses a la coordonnee dans leur propre conteneur : deux
// controles alignes a droite laisseraient leur ordre au hasard.
LActions := TLayout.Create(LCard);
LActions.Parent := LCard;
LActions.Align := TAlignLayout.Right;
LActions.Width := 150;
LActions.Margins.Rect := RectF(0, 8, 8, 8);
LBtnUser := FlatButton(LActions, 'User', OnRowCopyUser);
LBtnUser.Align := TAlignLayout.None;
LBtnUser.SetBounds(0, 0, 70, 54);
LBtnUser.Tag := FFiltered[I];
LBtnPwd := FlatButton(LActions, 'Pass', OnRowCopyPwd);
LBtnPwd.Align := TAlignLayout.None;
LBtnPwd.SetBounds(76, 0, 70, 54);
LBtnPwd.Tag := FFiltered[I];
// CONTENU CONSTRUIT A LA MAIN, sans passer par Text/Detail/Bitmap ni par // CONTENU CONSTRUIT A LA MAIN, sans passer par Text/Detail/Bitmap ni par
// un StyleLookup. Ces gabarits n'existent pas dans tous les styles : // un StyleLookup. Ces gabarits n'existent pas dans tous les styles :
// avec Dark.style, 'listboxitembottomdetail' disparait et emporte // avec Dark.style, 'listboxitembottomdetail' disparait et emporte
@@ -1476,8 +1512,8 @@ begin
// alignes Left et Client - il n'y a alors pas d'ordre a deviner. // alignes Left et Client - il n'y a alors pas d'ordre a deviner.
if FIcons.TryGetValue(FFiltered[I], LBmp) then if FIcons.TryGetValue(FFiltered[I], LBmp) then
begin begin
LImg := TImage.Create(LItem); LImg := TImage.Create(LCard);
LImg.Parent := LItem; LImg.Parent := LCard;
LImg.Align := TAlignLayout.Left; LImg.Align := TAlignLayout.Left;
LImg.Width := 40; LImg.Width := 40;
LImg.Margins.Rect := RectF(4, 8, 8, 8); LImg.Margins.Rect := RectF(4, 8, 8, 8);
@@ -1485,8 +1521,8 @@ begin
LImg.Bitmap.Assign(LBmp); LImg.Bitmap.Assign(LBmp);
end; end;
LBody := TLayout.Create(LItem); LBody := TLayout.Create(LCard);
LBody.Parent := LItem; LBody.Parent := LCard;
LBody.Align := TAlignLayout.Client; LBody.Align := TAlignLayout.Client;
LBody.Margins.Rect := RectF(8, 4, 8, 4); LBody.Margins.Rect := RectF(8, 4, 8, 4);
LBody.HitTest := False; LBody.HitTest := False;