I am implementing simple tools windows based on the TCastleShape. You can drag them around and close them. But I can’t figure out how to bring them to the front when you click on them. No matter what I do they are in the order created (using Insertfront). I have tried setting KeepInFront on the active window (and unsetting it on the old active window) but it doesn’t affect order of controls. This seems like it should be simple, so maybe I am just overlooking the solution. I have also tried to remove the control from the parent and readding it, but this causes crash when done from from the press handler. It seems like it should just be mycontrol.movetofront or something like that.
Changing KeepInFront
while the control is already added indeed does nothing.
It’s a trap, although documented —
{ Keep the control in front of other controls (with KeepInFront=@false)
when inserting.
TODO: Do not change this property while the control is already
a child of something. }
property KeepInFront: boolean read FKeepInFront write FKeepInFront
default false;
I hesitate what to do with KeepInFront
in general, maybe it should be deprecated.
Removing it + adding is indeed the proper solution. I see we even have our press handler “robust” to handle the case when control removes itself from the parent list… So, this should work.
Can you send a testcase that shows a crash? I’d like to fix it.
This is the extent of that window code:
TGameForm = class( TCastleShape )
constructor create( aowner : tcomponent ); override;
destructor destroy; override;
procedure PressClosebutton(const Sender: TCastleUserInterface;
const Event: TInputPressRelease; var Handled: Boolean);
procedure PressHeader(const Sender: TCastleUserInterface;
const Event: TInputPressRelease; var Handled: Boolean);
procedure ReleaseHeader(const Sender: TCastleUserInterface;
const Event: TInputPressRelease; var Handled: Boolean);
procedure HeaderMotion(const Sender: TCastleUserInterface;
const Event: TInputMotion;
var Handled: Boolean);
public
HeaderBar : TButtonBar;
Dragging : boolean;
end;
implementation
constructor TGameForm.create( aowner : tcomponent );
var button : TIconButton;
begin
inherited create( aowner );
VerticalAnchorParent := vpTop;
VerticalAnchorSelf := vpTop;
HorizontalAnchorParent := hpLeft;
HorizontalAnchorSelf := hpLeft;
HeaderBar := TButtonBar.create( self );
HeaderBar.Height := 20;
HeaderBar.VerticalAnchorParent := vptop;
HeaderBar.VerticalAnchorSelf := vptop;;
insertfront( HeaderBar );
button := TIconButton.create( HeaderBar );
button.setcaptiontooltip( 'x', 'close' );
button.OnPress := @PressCloseButton;
HeaderBar.InsertFront( button );
HeaderBar.Color := Vector4(0,0,0,0.5);
dragging := false;
HeaderBar.OnPress := @PressHeader;
HeaderBar.OnRelease := @ReleaseHeader;
HeaderBar.OnMotion := @HeaderMotion;
end;
destructor TGameForm.destroy;
begin
inherited;
end;
procedure TGameForm.PressClosebutton(const Sender: TCastleUserInterface;
const Event: TInputPressRelease; var Handled: Boolean);
begin
Handled := true;
parent.RemoveControl(self);
end;
procedure TGameForm.PressHeader(const Sender: TCastleUserInterface;
const Event: TInputPressRelease; var Handled: Boolean);
begin
Handled := true;
Dragging := true;
parent.RemoveControl(self);
parent.InsertFront(self);
end;
procedure TGameForm.ReleaseHeader(const Sender: TCastleUserInterface;
const Event: TInputPressRelease; var Handled: Boolean);
begin
Handled := true;
Dragging := false;
end;
procedure TGameForm.HeaderMotion(const Sender: TCastleUserInterface;
const Event: TInputMotion;
var Handled: Boolean);
var delta : TVector2;
begin
if Dragging then
begin
Handled := true;
delta := ( Event.Position - Event.OldPosition ) * 0.6;
Translation := Translation + delta;
end;
end;
The header is a TButtonBar from my Behavior Tree editor which is also a TShape. When you click the header it calls PressHeader and where the parent.removecontrol(self)
works but then the parent.insertfront(self)
crashes.
[update 1]
ohh. guessing parent is no longer valid. I think that is the problem.
[update 2]
Yes, that was the problem. I should have realized!!
Also, what is the correct way to use the mouse delta as the window delta (see HeaderMotion)? I have a 0.6 ‘kludge factor’ which works to keep the mouse and moving window synced when the program starts, but isn’t correct and becomes wrong if you change the application size.
Sorry for a delay in responding – I just got back to this thread, and I see the main issue is solved. Yeah, Parent
becomes nil
after the control removes itself from this parent. This is by design, it’s correct, albeit I see how it was unexpected in this context.
Simply doing something like
var
OldParent: TCastleUserInterface;
begin
...
OldParent := Parent;
OldParent.RemoveControl(Self);
OldParent.InsertFront(Self);
end;
should fix the issue, I guess that’s what you did now.
Thinking about it – adding a MoveToFront
method, like you suggested, that would deep down also really move in the list (so would not have any unexpected behavior due to remove + reinsert) seems like a good idea. Done! In Add TCastleUserInterface.MoveToFront · castle-engine/castle-engine@0b13a1c · GitHub .
So you can just call MoveToFront
within that TGameForm.PressHeader
now.
The Event.Position - Event.OldPosition
you get is in final device pixels. How to treat it depends on how do these pixels map to the coordinates map to what you want to shift, which is Translation
of TCastleShape
in this case. Ah, so just convert them to the “unscaled” coordinates, by UIScale
.
delta := ( Event.Position - Event.OldPosition ) / UIScale;
should be a replacement for
delta := ( Event.Position - Event.OldPosition ) * 0.6;
( I have a TODO to maybe express these things in unscaled coordinates, but it’s uneasy due to backward compatibility now. It will require inventing a better names than Position
for it Maybe Event.Translation
? And then we can deprecate Event.Position
without breaking its meaning. )