View Issue Details

IDProjectCategoryView StatusLast Update
0037942LazarusLCLpublic2020-10-26 05:30
ReporterJoeny Ang Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
Product Version2.1 (SVN) 
Summary0037942: TPageControl: TabStop, TabSwitch Focus and Keyboard TabSwitch
DescriptionAll Widgetsets:
- When TabStop = False, it should not receive focus
- nboKeyboardTabSwitch not implemented properly (switching tab via Ctrl-Tab/
  Ctrl-Shift-Tab)

Gtk2:
- when switching page, focus should be on the first control of the page
- TComboBox (csDropDownList) does not trigger onEnter/onExit when
  gtk_widget_focus_child() is called. This needs to be fixed in order to
  address the issue below
- switching to a page with a TComboBox, TRadioGroup or TCheckGroup will move
  focus to the corresponding combobox, radiogroup or checkgroup. This happens
  only when switching using the mouse, not when using SelectNextPage().

Changes made by patch:
1. TabStop issue (fixed GTK2 and Win32)
   - added TWSWinControl.SetTabStop() to be overriden by widgetsets, called
     by TWinControl.CMTabStopChanged() when TabStop is changed.
2. Keyboard TabSwitch issue
   - removed TCustomTabControl.KeyDown() function and moved implementation to
     TWinControl.DoKeyDownBeforeInterface(). This will look for the first
     tab control parent of the focused control and process the keys.
3. TabSwitch Focus issue (Gtk2)
   - caused by issue#20493 workaround in GtkWSNotebook_SwitchPage() and
     GtkWSNotebook_AfterSwitchPage(). Modified workaround (see unit1.pas
     source notes)
Steps To ReproduceTest projects:
- Project1: tab focus test
  - try switching back and forth between pages 1 & 2 using the mouse. Focus
    will go to the combobox on page 2.
- Project2: gtk_widget_focus_child() test (gtk2)
  - using gtk_widget_focus_child(), the combobox (list) does not trigger
    onEnter/onExit
- Project3: keyboard tabswitch test
TagsNo tags attached.
Fixed in Revision
LazTarget
Widgetset
Attached Files

Relationships

related to 0020493 closedZeljan Rikalo OnExit called in OnEnter 

Activities

Joeny Ang

2020-10-17 09:56

reporter  

tpagecontrol-quirks-fix.patch (15,078 bytes)   
--- lcl/comctrls.pp.64032
+++ lcl/comctrls.pp
@@ -434,13 +434,13 @@
     procedure SetOptions(const AValue: TCTabControlOptions); virtual;
     procedure AddRemovePageHandle(APage: TCustomPage); virtual;
     procedure CNNotify(var Message: TLMNotify); message CN_NOTIFY;
+    procedure CMTabStopChanged(var Message: TLMessage); message CM_TABSTOPCHANGED;
     class procedure WSRegisterClass; override;
     procedure CreateWnd; override;
     procedure Loaded; override;
     procedure DoChange; virtual;
     procedure InitializeWnd; override;
     procedure Change; virtual;
-    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
     procedure ReadState(Reader: TReader); override;
     function  DialogChar(var Message: TLMKey): boolean; override;
     procedure InternalSetPageIndex(AValue: Integer); // No OnChange
--- lcl/controls.pp.64032
+++ lcl/controls.pp
@@ -2790,6 +2790,7 @@
   WSControls, // circle with base widgetset is allowed
   WSLCLClasses,
   Forms, // the circle can't be broken without breaking Delphi compatibility
+  ComCtrls,
   Math;  // Math is in RTL and only a few functions are used.
 
 var
--- lcl/include/customnotebook.inc.64032
+++ lcl/include/customnotebook.inc
@@ -768,27 +768,6 @@
 function TCustomTabControl.IsStoredActivePage: boolean;
 begin
   Result:=false;
-end;
-
-procedure TCustomTabControl.KeyDown(var Key: Word; Shift: TShiftState);
-begin
-  if (nboKeyboardTabSwitch in Options) and (Key = VK_TAB) and (PageCount > 0) then 
-  begin
-    if Shift = [ssCtrl] then 
-    begin
-      Key := 0;
-      PageIndex := (PageIndex + 1) mod PageCount;
-      Exit;
-    end
-    else if Shift = [ssCtrl, ssShift] then 
-    begin
-      Key := 0;
-      PageIndex := (PageIndex + PageCount - 1) mod PageCount;
-      Exit;
-    end;
-  end;
-
-  inherited KeyDown(Key, Shift);
 end;
 
 {------------------------------------------------------------------------------
@@ -1102,6 +1081,16 @@
     end;
 end;
 
+
+{------------------------------------------------------------------------------
+  TCustomTabControl CMTabStopChanged
+ ------------------------------------------------------------------------------}
+procedure TCustomTabControl.CMTabStopChanged(var Message: TLMessage);
+begin
+  if HandleAllocated then
+    TWSCustomTabControlClass(WidgetSetClass).SetTabStop(Self, TabStop);
+end;
+
 {------------------------------------------------------------------------------
   procedure TCustomTabControl.ShowCurrentPage
 
--- lcl/include/wincontrol.inc.64032
+++ lcl/include/wincontrol.inc
@@ -5784,6 +5784,7 @@
 
 var
   F: TCustomForm;
+  T: TCustomTabControl;
   ShiftState: TShiftState;
   AParent: TWinControl;
 begin
@@ -5818,6 +5819,33 @@
 
       if CharCode = VK_UNKNOWN then Exit;
       ShiftState := KeyDataToShiftState(KeyData);
+
+      // for Ctrl+Tab/Shift+Ctrl+Tab key combi: if self or any parent is a
+      // TCustomTabControl with nboKeyboardTabSwitch option, process it
+      AParent := Self;
+      while Assigned(AParent) do
+      begin
+        if (AParent is TCustomTabControl) or (AParent is TTabControl) then
+        begin
+          if AParent is TTabControl then
+            T := TTabControlNoteBookStrings(TTabControl(AParent).Tabs).NoteBook
+          else
+            T := TCustomTabControl(AParent);
+          if (nboKeyboardTabSwitch in T.Options) and (T.PageCount > 0) and
+            (CharCode = VK_TAB) then
+            if ShiftState = [ssCtrl] then
+            begin
+              T.PageIndex := (T.PageIndex + 1) mod T.PageCount;
+              Exit;
+            end
+            else if ShiftState = [ssCtrl, ssShift] then
+            begin
+              T.PageIndex := (T.PageIndex + T.PageCount - 1) mod T.PageCount;
+              Exit;
+            end;
+        end;
+        AParent := AParent.Parent;
+      end;
 
       // let drag object handle the key
       if DragManager.IsDragging then
--- lcl/widgetset/wscontrols.pp.64032
+++ lcl/widgetset/wscontrols.pp
@@ -121,6 +121,7 @@
     class procedure SetFont(const AWinControl: TWinControl; const AFont: TFont); virtual;
     class procedure SetPos(const AWinControl: TWinControl; const ALeft, ATop: Integer); virtual;
     class procedure SetSize(const AWinControl: TWinControl; const AWidth, AHeight: Integer); virtual;
+    class procedure SetTabStop(const AWinControl: TWinControl; const AValue: Boolean); virtual;
     class procedure SetText(const AWinControl: TWinControl; const AText: String); virtual;
     class procedure SetCursor(const AWinControl: TWinControl; const ACursor: HCursor); virtual;
     class procedure SetShape(const AWinControl: TWinControl; const AShape: HBITMAP); virtual;
@@ -394,6 +395,11 @@
 begin
 end;
 
+class procedure TWSWinControl.SetTabStop(const AWinControl: TWinControl;
+  const AValue: Boolean);
+begin
+end;
+
 {------------------------------------------------------------------------------
   Method: TWSWinControl.SetLabel
   Params:  AWinControl - the calling object
--- lcl/interfaces/gtk2/gtk2callback.inc.64032
+++ lcl/interfaces/gtk2/gtk2callback.inc
@@ -1783,19 +1783,6 @@
     gtk_list_item_select(PGtkListItem(List^.Data));
   end;
 
-  procedure FixTabControlFocusBehaviour;
-  var
-    Info: PWidgetInfo;
-  begin
-    {gtk_notebook have weird behaviour when clicked.
-     if there's active control on page it'll loose it's
-     focus and trigger OnExit (tab is taking focus).
-     issue #20493}
-    Info := GetWidgetInfo(Widget);
-    if not gtk_widget_is_focus(Widget) then
-      Include(Info^.Flags, wwiTabWidgetFocusCheck);
-  end;
-
 var
   DesignOnlySignal: boolean;
   Msg: TLMContextMenu;
@@ -1852,9 +1839,6 @@
     if Event^.button = 1 then
     begin
       //CaptureMouseForWidget(CaptureWidget,mctGTKIntf);
-      if (TControl(Data) is TCustomTabControl) and
-        not (csDesigning in TControl(Data).ComponentState) then
-          FixTabControlFocusBehaviour;
     end
     else
     // if LCL process LM_CONTEXTMENU then stop the event propagation
--- lcl/interfaces/gtk2/gtk2pagecontrol.inc.64032
+++ lcl/interfaces/gtk2/gtk2pagecontrol.inc
@@ -78,14 +78,14 @@
   end;
 end;
 
-function GtkRestoreFocusFix(AGtkWidget: Pointer): gboolean; cdecl;
-begin
-  Result := AGtkWidget <> nil;
-  if AGtkWidget <> nil then
-  begin
-    GTK_WIDGET_SET_FLAGS(PGtkWidget(AGtkWidget), GTK_CAN_FOCUS);
-    g_idle_remove_by_data(AGtkWidget);
-  end;
+function GtkNotebookPostSwitchPage(AGtkWidget: Pointer): gboolean; cdecl;
+begin
+  Result := False;    // automatically remove from idle list
+  if AGtkWidget = nil then
+    Exit;
+  // select first child widget if tab control has focus
+  if gtk_widget_has_focus(AGtkWidget) then
+    gtk_widget_child_focus(AGtkWidget, GTK_DIR_DOWN);
 end;
 
 function GtkWSNotebook_AfterSwitchPage(widget: PGtkWidget; {%H-}page: Pgtkwidget; pagenum: integer; data: gPointer): GBoolean; cdecl;
@@ -93,12 +93,8 @@
   Mess: TLMNotify;
   NMHdr: tagNMHDR;
   Info: PWidgetInfo;
-  ACtl: TWinControl;
-  AParentForm: TCustomForm;
-  i: Integer;
   LCLPageIndex: Integer;
-  Pg: TCustomPage;
-  ChildWidget: PGtkWidget;
+  FTabStop, FWasFocused: Boolean;
 begin
   Result := CallBackDefaultReturn;
   // then send the new page
@@ -112,67 +108,21 @@
   Mess.NMHdr := @NMHdr;
   DeliverMessage(Data, Mess);
 
-  // code below is fix for issue #20493
-  Info := GetWidgetInfo(Widget);
-  if wwiTabWidgetFocusCheck in Info^.Flags then
-  begin
-    Exclude(Info^.Flags, wwiTabWidgetFocusCheck);
-
-    if LCLPageIndex = -1 then
-      exit;
-
-    ACtl := TWinControl(Data);
-    AParentForm := GetParentForm(ACtl);
-    if Assigned(AParentForm) then
-    begin
-      // 1st we must find focused control (if any)
-      ACtl := nil;
-      if (LCLPageIndex >= 0) and (LCLPageIndex < TCustomTabControl(Data).PageCount) then
-        Pg := TCustomTabControl(Data).Page[LCLPageIndex]
-      else
-        Pg := nil;
-      if Assigned(Pg) then
-      begin
-        for i := 0 to Pg.ControlCount - 1 do
-        begin
-          if (pg.Controls[i] is TWinControl) and
-            (TWinControl(pg.Controls[i]).Focused) then
-          begin
-            ACtl := TWinControl(pg.Controls[i]);
-            break;
-          end;
-        end;
-      end;
-      if (ACtl = nil) and (Pg <> nil) then
-        ACtl := AParentForm.ActiveControl;
-    end else
-      ACtl := nil;
-
-    if (ACtl <> nil) and (ACtl <> TWinControl(Data)) then
-    begin
-      // DebugLn('ActiveCtl is ',ACtl.ClassName,':',ACtl.Name);
-      // do not focus tab by mouse click if we already have active control
+  if not (TObject(Data) is TNoteBookStringsTabControl) then
+  begin
+    // flags
+    Info := GetWidgetInfo(Widget);
+    FWasFocused := wwiTabWidgetFocusCheck in Info^.Flags;
+    if FWasFocused then
+      Exclude(Info^.Flags, wwiTabWidgetFocusCheck);
+    FTabStop := TWinControl(Data).TabStop;
+    if not FTabStop or (FTabStop and not FWasFocused) then
+      // post switch page function: will select first widget child if
+      // tab control has focus
+      g_idle_add(@GtkNotebookPostSwitchPage, Widget);
+    // restore GTK_CAN_FOCUS based on TabStop
+    if not FTabStop then
       GTK_WIDGET_UNSET_FLAGS(Widget, GTK_CAN_FOCUS);
-      Pg := TCustomTabControl(Data).Page[LCLPageIndex];
-      for i := 0 to Pg.ControlCount - 1 do
-      begin
-        // we must prevent gtkWidget to acquire focus by gtk (eg. GtkButton)
-        if (Pg.Controls[i] is TWinControl) and (Pg.Controls[i] <> ACtl) then
-        begin
-          Info := GetWidgetInfo({%H-}PGtkWidget(TWinControl(Pg.Controls[i]).Handle));
-          if Info <> nil then
-          begin
-            if Info^.CoreWidget <> nil then
-              ChildWidget := Info^.CoreWidget
-            else
-              ChildWidget := Info^.ClientWidget;
-            GTK_WIDGET_UNSET_FLAGS(ChildWidget, GTK_CAN_FOCUS);
-            g_idle_add(@GtkRestoreFocusFix, ChildWidget);
-          end;
-        end;
-      end;
-      g_idle_add(@GtkRestoreFocusFix, Widget);
-    end;
   end;
 end;
 
@@ -181,6 +131,7 @@
   Mess: TLMNotify;
   NMHdr: tagNMHDR;
   IsManual: Boolean;
+  Info: PWidgetInfo;
 begin
   Result := CallBackDefaultReturn;
   EventTrace('switch-page', data);
@@ -192,6 +143,25 @@
     g_object_set_data(PGObject(Widget), LCL_NotebookManualPageSwitchKey, nil);
   if PGtkNotebook(Widget)^.cur_page = nil then // for windows compatibility
     Exit;
+
+  // Note: not applicable to TTabControl (TNoteBookStringsTabControl child)
+  if not (TObject(Data) is TNoteBookStringsTabControl) then
+  begin
+    Info := GetWidgetInfo(Widget);
+    // set temporary flags
+    if gtk_widget_has_focus(Widget) then
+      Include(Info^.Flags, wwiTabWidgetFocusCheck);
+
+    // when switching pages using mouse, gtk2 will automatically set focus
+    // to the tab control, then call gtk_widget_child_focus() to select the
+    // first child widget. if GTK_CAN_FOCUS is disabled (ie. TabStop=False),
+    // focus will be set on the first control, and gtk_widget_child_focus()
+    // will focus the next control.
+
+    // temporary enable GTK_CAN_FOCUS and let tab control have focus
+    GTK_WIDGET_SET_FLAGS(Widget, GTK_CAN_FOCUS);
+    gtk_widget_grab_focus(Widget);
+  end;
 
   // gtkswitchpage is called before the switch
   if not IsManual then
@@ -309,6 +279,8 @@
   Result := HWND(TLCLIntfHandle({%H-}PtrUInt(AWidget)));
   Set_RC_Name(AWinControl, PGtkWidget(AWidget));
   SetCallBacks(PGtkWidget(AWidget), WidgetInfo);
+  if not AWinControl.TabStop then
+    GTK_WIDGET_UNSET_FLAGS(PGtkWidget(AWidget), GTK_CAN_FOCUS);
 end;
 
 class function TGtk2WSCustomTabControl.GetDefaultClientRect(
@@ -599,6 +571,15 @@
     GtkPositionTypeMap[ATabPosition]);
 end;
 
+class procedure TGtk2WSCustomTabControl.SetTabStop(const AWinControl: TWinControl;
+  const AValue: Boolean);
+begin
+  if not AValue then
+    GTK_WIDGET_UNSET_FLAGS({%H-}PGtkWidget(AWinControl.Handle), GTK_CAN_FOCUS)
+  else
+    GTK_WIDGET_SET_FLAGS({%H-}PGtkWidget(AWinControl.Handle), GTK_CAN_FOCUS);
+end;
+
 class procedure TGtk2WSCustomTabControl.ShowTabs(const ATabControl: TCustomTabControl;
   AShowTabs: boolean);
 begin
--- lcl/interfaces/gtk2/gtk2wscomctrls.pp.64032
+++ lcl/interfaces/gtk2/gtk2wscomctrls.pp
@@ -96,6 +96,7 @@
     class function GetTabRect(const ATabControl: TCustomTabControl; const AIndex: Integer): TRect; override;
     class procedure SetPageIndex(const ATabControl: TCustomTabControl; const AIndex: integer); override;
     class procedure SetTabPosition(const ATabControl: TCustomTabControl; const ATabPosition: TTabPosition); override;
+    class procedure SetTabStop(const AWinControl: TWinControl; const AValue: Boolean); override;
     class procedure ShowTabs(const ATabControl: TCustomTabControl; AShowTabs: boolean); override;
     class procedure UpdateProperties(const ATabControl: TCustomTabControl); override;
   end;
--- lcl/interfaces/gtk2/gtk2wsstdctrls.pp.64032
+++ lcl/interfaces/gtk2/gtk2wsstdctrls.pp
@@ -1741,9 +1741,12 @@
     Gtk2WidgetSet.SetCallbackDirect(LM_FOCUS, AButton, AWinControl);
   end;
   
-  // if we are a GtkComboBoxEntry
-  if not GtkWidgetIsA(PGtkWidget(AEntry), GTK_TYPE_ENTRY) then
-    g_signal_connect(Combowidget, 'grab-focus', TGCallback(@GtkComboFocus), AWidgetInfo);
+  // if we are a GtkComboBoxEntry, do not allow dropdown button to have focus
+  if GtkWidgetIsA(PGtkWidget(AEntry), GTK_TYPE_ENTRY) then
+    GTK_WIDGET_UNSET_FLAGS(APrivate^.box, GTK_CAN_FOCUS)
+  else
+    // if we are a GtkComboBox, attach a callback to trigger onEnter/onExit
+    g_signal_connect(APrivate^.box, 'grab-focus', TGCallback(@GtkComboFocus), AWidgetInfo);
 
   AMenu := nil;
   if (APrivate^.popup_widget <> nil)
--- lcl/interfaces/win32/win32pagecontrol.inc.64032
+++ lcl/interfaces/win32/win32pagecontrol.inc
@@ -752,6 +752,16 @@
     RecreateWnd(ATabControl);
 end;
 
+class procedure TWin32WSCustomTabControl.SetTabStop(const ATabControl: TWinControl; const AValue: Boolean);
+begin
+  if not (csDestroying in ATabControl.ComponentState) then
+  begin
+    ATabControl.TabStop := AValue;
+    if ATabControl.HandleAllocated then
+      RecreateWnd(ATabControl);
+  end;
+end;
+
 class procedure TWin32WSCustomTabControl.ShowTabs(const ATabControl: TCustomTabControl; AShowTabs: boolean);
 begin
   if ATabControl is TTabControl then
--- lcl/interfaces/win32/win32wscomctrls.pp.64032
+++ lcl/interfaces/win32/win32wscomctrls.pp
@@ -75,6 +75,7 @@
     class procedure SetImageList(const ATabControl: TCustomTabControl; const AImageList: TCustomImageListResolution); override;
     class procedure SetPageIndex(const ATabControl: TCustomTabControl; const AIndex: integer); override;
     class procedure SetTabPosition(const ATabControl: TCustomTabControl; const ATabPosition: TTabPosition); override;
+    class procedure SetTabStop(const ATabControl: TWinControl; const AValue: Boolean); override;
     class procedure ShowTabs(const ATabControl: TCustomTabControl; AShowTabs: boolean); override;
     class procedure UpdateProperties(const ATabControl: TCustomTabControl); override;
   end;

tpagecontrol-quirks-fix.patch (15,078 bytes)   

Joeny Ang

2020-10-20 10:35

reporter   ~0126421

Updated patch:
- Fix to "invalid unclassed pointer in cast to 'GtkObject'" error raised when
form containing a TPageControl with more than 1 page is closed.
tpagecontrol-quirks-fix-v2.patch (15,113 bytes)   
--- lcl/comctrls.pp.64032
+++ lcl/comctrls.pp
@@ -434,13 +434,13 @@
     procedure SetOptions(const AValue: TCTabControlOptions); virtual;
     procedure AddRemovePageHandle(APage: TCustomPage); virtual;
     procedure CNNotify(var Message: TLMNotify); message CN_NOTIFY;
+    procedure CMTabStopChanged(var Message: TLMessage); message CM_TABSTOPCHANGED;
     class procedure WSRegisterClass; override;
     procedure CreateWnd; override;
     procedure Loaded; override;
     procedure DoChange; virtual;
     procedure InitializeWnd; override;
     procedure Change; virtual;
-    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
     procedure ReadState(Reader: TReader); override;
     function  DialogChar(var Message: TLMKey): boolean; override;
     procedure InternalSetPageIndex(AValue: Integer); // No OnChange
--- lcl/controls.pp.64032
+++ lcl/controls.pp
@@ -2790,6 +2790,7 @@
   WSControls, // circle with base widgetset is allowed
   WSLCLClasses,
   Forms, // the circle can't be broken without breaking Delphi compatibility
+  ComCtrls,
   Math;  // Math is in RTL and only a few functions are used.
 
 var
--- lcl/include/customnotebook.inc.64032
+++ lcl/include/customnotebook.inc
@@ -768,27 +768,6 @@
 function TCustomTabControl.IsStoredActivePage: boolean;
 begin
   Result:=false;
-end;
-
-procedure TCustomTabControl.KeyDown(var Key: Word; Shift: TShiftState);
-begin
-  if (nboKeyboardTabSwitch in Options) and (Key = VK_TAB) and (PageCount > 0) then 
-  begin
-    if Shift = [ssCtrl] then 
-    begin
-      Key := 0;
-      PageIndex := (PageIndex + 1) mod PageCount;
-      Exit;
-    end
-    else if Shift = [ssCtrl, ssShift] then 
-    begin
-      Key := 0;
-      PageIndex := (PageIndex + PageCount - 1) mod PageCount;
-      Exit;
-    end;
-  end;
-
-  inherited KeyDown(Key, Shift);
 end;
 
 {------------------------------------------------------------------------------
@@ -1102,6 +1081,16 @@
     end;
 end;
 
+
+{------------------------------------------------------------------------------
+  TCustomTabControl CMTabStopChanged
+ ------------------------------------------------------------------------------}
+procedure TCustomTabControl.CMTabStopChanged(var Message: TLMessage);
+begin
+  if HandleAllocated then
+    TWSCustomTabControlClass(WidgetSetClass).SetTabStop(Self, TabStop);
+end;
+
 {------------------------------------------------------------------------------
   procedure TCustomTabControl.ShowCurrentPage
 
--- lcl/include/wincontrol.inc.64032
+++ lcl/include/wincontrol.inc
@@ -5784,6 +5784,7 @@
 
 var
   F: TCustomForm;
+  T: TCustomTabControl;
   ShiftState: TShiftState;
   AParent: TWinControl;
 begin
@@ -5818,6 +5819,33 @@
 
       if CharCode = VK_UNKNOWN then Exit;
       ShiftState := KeyDataToShiftState(KeyData);
+
+      // for Ctrl+Tab/Shift+Ctrl+Tab key combi: if self or any parent is a
+      // TCustomTabControl with nboKeyboardTabSwitch option, process it
+      AParent := Self;
+      while Assigned(AParent) do
+      begin
+        if (AParent is TCustomTabControl) or (AParent is TTabControl) then
+        begin
+          if AParent is TTabControl then
+            T := TTabControlNoteBookStrings(TTabControl(AParent).Tabs).NoteBook
+          else
+            T := TCustomTabControl(AParent);
+          if (nboKeyboardTabSwitch in T.Options) and (T.PageCount > 0) and
+            (CharCode = VK_TAB) then
+            if ShiftState = [ssCtrl] then
+            begin
+              T.PageIndex := (T.PageIndex + 1) mod T.PageCount;
+              Exit;
+            end
+            else if ShiftState = [ssCtrl, ssShift] then
+            begin
+              T.PageIndex := (T.PageIndex + T.PageCount - 1) mod T.PageCount;
+              Exit;
+            end;
+        end;
+        AParent := AParent.Parent;
+      end;
 
       // let drag object handle the key
       if DragManager.IsDragging then
--- lcl/widgetset/wscontrols.pp.64032
+++ lcl/widgetset/wscontrols.pp
@@ -121,6 +121,7 @@
     class procedure SetFont(const AWinControl: TWinControl; const AFont: TFont); virtual;
     class procedure SetPos(const AWinControl: TWinControl; const ALeft, ATop: Integer); virtual;
     class procedure SetSize(const AWinControl: TWinControl; const AWidth, AHeight: Integer); virtual;
+    class procedure SetTabStop(const AWinControl: TWinControl; const AValue: Boolean); virtual;
     class procedure SetText(const AWinControl: TWinControl; const AText: String); virtual;
     class procedure SetCursor(const AWinControl: TWinControl; const ACursor: HCursor); virtual;
     class procedure SetShape(const AWinControl: TWinControl; const AShape: HBITMAP); virtual;
@@ -394,6 +395,11 @@
 begin
 end;
 
+class procedure TWSWinControl.SetTabStop(const AWinControl: TWinControl;
+  const AValue: Boolean);
+begin
+end;
+
 {------------------------------------------------------------------------------
   Method: TWSWinControl.SetLabel
   Params:  AWinControl - the calling object
--- lcl/interfaces/gtk2/gtk2callback.inc.64032
+++ lcl/interfaces/gtk2/gtk2callback.inc
@@ -1783,19 +1783,6 @@
     gtk_list_item_select(PGtkListItem(List^.Data));
   end;
 
-  procedure FixTabControlFocusBehaviour;
-  var
-    Info: PWidgetInfo;
-  begin
-    {gtk_notebook have weird behaviour when clicked.
-     if there's active control on page it'll loose it's
-     focus and trigger OnExit (tab is taking focus).
-     issue #20493}
-    Info := GetWidgetInfo(Widget);
-    if not gtk_widget_is_focus(Widget) then
-      Include(Info^.Flags, wwiTabWidgetFocusCheck);
-  end;
-
 var
   DesignOnlySignal: boolean;
   Msg: TLMContextMenu;
@@ -1852,9 +1839,6 @@
     if Event^.button = 1 then
     begin
       //CaptureMouseForWidget(CaptureWidget,mctGTKIntf);
-      if (TControl(Data) is TCustomTabControl) and
-        not (csDesigning in TControl(Data).ComponentState) then
-          FixTabControlFocusBehaviour;
     end
     else
     // if LCL process LM_CONTEXTMENU then stop the event propagation
--- lcl/interfaces/gtk2/gtk2pagecontrol.inc.64032
+++ lcl/interfaces/gtk2/gtk2pagecontrol.inc
@@ -78,14 +78,14 @@
   end;
 end;
 
-function GtkRestoreFocusFix(AGtkWidget: Pointer): gboolean; cdecl;
-begin
-  Result := AGtkWidget <> nil;
-  if AGtkWidget <> nil then
-  begin
-    GTK_WIDGET_SET_FLAGS(PGtkWidget(AGtkWidget), GTK_CAN_FOCUS);
-    g_idle_remove_by_data(AGtkWidget);
-  end;
+function GtkNotebookPostSwitchPage(AGtkWidget: Pointer): gboolean; cdecl;
+begin
+  Result := False;    // automatically remove from idle list
+  if (AGtkWidget = nil) or not GTK_IS_WIDGET(AGtkWidget) then
+    Exit;
+  // select first child widget if tab control has focus
+  if gtk_widget_has_focus(AGtkWidget) then
+    gtk_widget_child_focus(AGtkWidget, GTK_DIR_DOWN);
 end;
 
 function GtkWSNotebook_AfterSwitchPage(widget: PGtkWidget; {%H-}page: Pgtkwidget; pagenum: integer; data: gPointer): GBoolean; cdecl;
@@ -93,12 +93,8 @@
   Mess: TLMNotify;
   NMHdr: tagNMHDR;
   Info: PWidgetInfo;
-  ACtl: TWinControl;
-  AParentForm: TCustomForm;
-  i: Integer;
   LCLPageIndex: Integer;
-  Pg: TCustomPage;
-  ChildWidget: PGtkWidget;
+  FTabStop, FWasFocused: Boolean;
 begin
   Result := CallBackDefaultReturn;
   // then send the new page
@@ -112,67 +108,21 @@
   Mess.NMHdr := @NMHdr;
   DeliverMessage(Data, Mess);
 
-  // code below is fix for issue #20493
-  Info := GetWidgetInfo(Widget);
-  if wwiTabWidgetFocusCheck in Info^.Flags then
-  begin
-    Exclude(Info^.Flags, wwiTabWidgetFocusCheck);
-
-    if LCLPageIndex = -1 then
-      exit;
-
-    ACtl := TWinControl(Data);
-    AParentForm := GetParentForm(ACtl);
-    if Assigned(AParentForm) then
-    begin
-      // 1st we must find focused control (if any)
-      ACtl := nil;
-      if (LCLPageIndex >= 0) and (LCLPageIndex < TCustomTabControl(Data).PageCount) then
-        Pg := TCustomTabControl(Data).Page[LCLPageIndex]
-      else
-        Pg := nil;
-      if Assigned(Pg) then
-      begin
-        for i := 0 to Pg.ControlCount - 1 do
-        begin
-          if (pg.Controls[i] is TWinControl) and
-            (TWinControl(pg.Controls[i]).Focused) then
-          begin
-            ACtl := TWinControl(pg.Controls[i]);
-            break;
-          end;
-        end;
-      end;
-      if (ACtl = nil) and (Pg <> nil) then
-        ACtl := AParentForm.ActiveControl;
-    end else
-      ACtl := nil;
-
-    if (ACtl <> nil) and (ACtl <> TWinControl(Data)) then
-    begin
-      // DebugLn('ActiveCtl is ',ACtl.ClassName,':',ACtl.Name);
-      // do not focus tab by mouse click if we already have active control
+  if not (TObject(Data) is TNoteBookStringsTabControl) then
+  begin
+    // flags
+    Info := GetWidgetInfo(Widget);
+    FWasFocused := wwiTabWidgetFocusCheck in Info^.Flags;
+    if FWasFocused then
+      Exclude(Info^.Flags, wwiTabWidgetFocusCheck);
+    FTabStop := TWinControl(Data).TabStop;
+    if not FTabStop or (FTabStop and not FWasFocused) then
+      // post switch page function: will select first widget child if
+      // tab control has focus
+      g_idle_add(@GtkNotebookPostSwitchPage, Widget);
+    // restore GTK_CAN_FOCUS based on TabStop
+    if not FTabStop then
       GTK_WIDGET_UNSET_FLAGS(Widget, GTK_CAN_FOCUS);
-      Pg := TCustomTabControl(Data).Page[LCLPageIndex];
-      for i := 0 to Pg.ControlCount - 1 do
-      begin
-        // we must prevent gtkWidget to acquire focus by gtk (eg. GtkButton)
-        if (Pg.Controls[i] is TWinControl) and (Pg.Controls[i] <> ACtl) then
-        begin
-          Info := GetWidgetInfo({%H-}PGtkWidget(TWinControl(Pg.Controls[i]).Handle));
-          if Info <> nil then
-          begin
-            if Info^.CoreWidget <> nil then
-              ChildWidget := Info^.CoreWidget
-            else
-              ChildWidget := Info^.ClientWidget;
-            GTK_WIDGET_UNSET_FLAGS(ChildWidget, GTK_CAN_FOCUS);
-            g_idle_add(@GtkRestoreFocusFix, ChildWidget);
-          end;
-        end;
-      end;
-      g_idle_add(@GtkRestoreFocusFix, Widget);
-    end;
   end;
 end;
 
@@ -181,6 +131,7 @@
   Mess: TLMNotify;
   NMHdr: tagNMHDR;
   IsManual: Boolean;
+  Info: PWidgetInfo;
 begin
   Result := CallBackDefaultReturn;
   EventTrace('switch-page', data);
@@ -192,6 +143,25 @@
     g_object_set_data(PGObject(Widget), LCL_NotebookManualPageSwitchKey, nil);
   if PGtkNotebook(Widget)^.cur_page = nil then // for windows compatibility
     Exit;
+
+  // Note: not applicable to TTabControl (TNoteBookStringsTabControl child)
+  if not (TObject(Data) is TNoteBookStringsTabControl) then
+  begin
+    Info := GetWidgetInfo(Widget);
+    // set temporary flags
+    if gtk_widget_has_focus(Widget) then
+      Include(Info^.Flags, wwiTabWidgetFocusCheck);
+
+    // when switching pages using mouse, gtk2 will automatically set focus
+    // to the tab control, then call gtk_widget_child_focus() to select the
+    // first child widget. if GTK_CAN_FOCUS is disabled (ie. TabStop=False),
+    // focus will be set on the first control, and gtk_widget_child_focus()
+    // will focus the next control.
+
+    // temporary enable GTK_CAN_FOCUS and let tab control have focus
+    GTK_WIDGET_SET_FLAGS(Widget, GTK_CAN_FOCUS);
+    gtk_widget_grab_focus(Widget);
+  end;
 
   // gtkswitchpage is called before the switch
   if not IsManual then
@@ -309,6 +279,8 @@
   Result := HWND(TLCLIntfHandle({%H-}PtrUInt(AWidget)));
   Set_RC_Name(AWinControl, PGtkWidget(AWidget));
   SetCallBacks(PGtkWidget(AWidget), WidgetInfo);
+  if not AWinControl.TabStop then
+    GTK_WIDGET_UNSET_FLAGS(PGtkWidget(AWidget), GTK_CAN_FOCUS);
 end;
 
 class function TGtk2WSCustomTabControl.GetDefaultClientRect(
@@ -599,6 +571,15 @@
     GtkPositionTypeMap[ATabPosition]);
 end;
 
+class procedure TGtk2WSCustomTabControl.SetTabStop(const AWinControl: TWinControl;
+  const AValue: Boolean);
+begin
+  if not AValue then
+    GTK_WIDGET_UNSET_FLAGS({%H-}PGtkWidget(AWinControl.Handle), GTK_CAN_FOCUS)
+  else
+    GTK_WIDGET_SET_FLAGS({%H-}PGtkWidget(AWinControl.Handle), GTK_CAN_FOCUS);
+end;
+
 class procedure TGtk2WSCustomTabControl.ShowTabs(const ATabControl: TCustomTabControl;
   AShowTabs: boolean);
 begin
--- lcl/interfaces/gtk2/gtk2wscomctrls.pp.64032
+++ lcl/interfaces/gtk2/gtk2wscomctrls.pp
@@ -96,6 +96,7 @@
     class function GetTabRect(const ATabControl: TCustomTabControl; const AIndex: Integer): TRect; override;
     class procedure SetPageIndex(const ATabControl: TCustomTabControl; const AIndex: integer); override;
     class procedure SetTabPosition(const ATabControl: TCustomTabControl; const ATabPosition: TTabPosition); override;
+    class procedure SetTabStop(const AWinControl: TWinControl; const AValue: Boolean); override;
     class procedure ShowTabs(const ATabControl: TCustomTabControl; AShowTabs: boolean); override;
     class procedure UpdateProperties(const ATabControl: TCustomTabControl); override;
   end;
--- lcl/interfaces/gtk2/gtk2wsstdctrls.pp.64032
+++ lcl/interfaces/gtk2/gtk2wsstdctrls.pp
@@ -1741,9 +1741,12 @@
     Gtk2WidgetSet.SetCallbackDirect(LM_FOCUS, AButton, AWinControl);
   end;
   
-  // if we are a GtkComboBoxEntry
-  if not GtkWidgetIsA(PGtkWidget(AEntry), GTK_TYPE_ENTRY) then
-    g_signal_connect(Combowidget, 'grab-focus', TGCallback(@GtkComboFocus), AWidgetInfo);
+  // if we are a GtkComboBoxEntry, do not allow dropdown button to have focus
+  if GtkWidgetIsA(PGtkWidget(AEntry), GTK_TYPE_ENTRY) then
+    GTK_WIDGET_UNSET_FLAGS(APrivate^.box, GTK_CAN_FOCUS)
+  else
+    // if we are a GtkComboBox, attach a callback to trigger onEnter/onExit
+    g_signal_connect(APrivate^.box, 'grab-focus', TGCallback(@GtkComboFocus), AWidgetInfo);
 
   AMenu := nil;
   if (APrivate^.popup_widget <> nil)
--- lcl/interfaces/win32/win32pagecontrol.inc.64032
+++ lcl/interfaces/win32/win32pagecontrol.inc
@@ -752,6 +752,16 @@
     RecreateWnd(ATabControl);
 end;
 
+class procedure TWin32WSCustomTabControl.SetTabStop(const ATabControl: TWinControl; const AValue: Boolean);
+begin
+  if not (csDestroying in ATabControl.ComponentState) then
+  begin
+    ATabControl.TabStop := AValue;
+    if ATabControl.HandleAllocated then
+      RecreateWnd(ATabControl);
+  end;
+end;
+
 class procedure TWin32WSCustomTabControl.ShowTabs(const ATabControl: TCustomTabControl; AShowTabs: boolean);
 begin
   if ATabControl is TTabControl then
--- lcl/interfaces/win32/win32wscomctrls.pp.64032
+++ lcl/interfaces/win32/win32wscomctrls.pp
@@ -75,6 +75,7 @@
     class procedure SetImageList(const ATabControl: TCustomTabControl; const AImageList: TCustomImageListResolution); override;
     class procedure SetPageIndex(const ATabControl: TCustomTabControl; const AIndex: integer); override;
     class procedure SetTabPosition(const ATabControl: TCustomTabControl; const ATabPosition: TTabPosition); override;
+    class procedure SetTabStop(const ATabControl: TWinControl; const AValue: Boolean); override;
     class procedure ShowTabs(const ATabControl: TCustomTabControl; AShowTabs: boolean); override;
     class procedure UpdateProperties(const ATabControl: TCustomTabControl); override;
   end;

jamie philbrook

2020-10-20 23:56

reporter   ~0126436

that is a lot of changes, also I notice changes in the LCL which effect everyone...

is this DELPHI compliant ?

does Delphi act in the manner of your changes ?

I've never had issues with the Tpagecontrol and I use it greatly on windows that is..

Joeny Ang

2020-10-21 10:14

reporter   ~0126439

Last edited: 2020-10-21 10:49

View 2 revisions

Hi Jamie :)

These are mostly fixes for the GTK2 'jumping focus' while switching page quirk.

I have tested Delphi 3 version of TPageControl and it behaved similarly except
for the keyboard tab switch issue (nboKeyboardTabSwitch option).

Delphi does not have this option; it automatically maps Ctrl-Tab/Ctrl-Shift-Tab
to the top most pagecontrol. Pressing the key combinations while any of its
child is in focus will trigger a page switch. And if you are inside a
pagecontrol that is inside a tabsheet, the keys does nothing (ie. pagecontrol
inside a pagecontrol).

The patch expanded this functionality to iterate through the parents of the
focused child control looking for the first parent that is a pagecontrol with
nboKeyboardTabSwitch option and process the keys there.

Regarding TabStop... implementing TWSWinControl.SetTabStop() just provided a
way to toggle TabStop programatically. Under Win32, it sets the TCS_FOCUSNEVER
flag, in GTK2, the GTK_CAN_FOCUS flag.

Juha Manninen

2020-10-21 11:25

developer   ~0126441

It bothers me a little that unit Controls adds a circular reference to ComCtrls. Lots of effort has been put to minimize such references.
The patch needs it at least in function DoKeyDownBeforeInterface(). Is there a way to implement it without an extra dependency?

Joeny Ang

2020-10-21 12:26

reporter   ~0126443

Hi Juha, so that's what the comment about "circle can't be broken..." was about... I did not paid attention haha :) I'll see what I can come up with. Thanks.

Joeny Ang

2020-10-26 03:58

reporter   ~0126560

Last edited: 2020-10-26 05:30

View 2 revisions

Hi, updated the patch :)

- moved keyboard tab switch implementation to application.inc: added
  TApplication.DoCtrlTabKey() function.
- fixed gtk2 page control not receiving Ctrl-Tab/Ctrl-Shift-Tab keys (in gtk2proc.inc)
- modified TCustomTabControl.ShowCurrentPage -> HasFocusedControl() to search
  APage recursively. This affects Win32 on instances where focused control is
  inside a pagecontrol inside another pagecontrol. Pressing the key combi will
  switch focus to first control of the parent form instead of the first pagecontrol.
- removed value assignment in TWin32WSCustomTabControl.SetTabStop()
tpagecontrol-quirks-fix-v4.patch (17,081 bytes)   
--- lcl/comctrls.pp.64032
+++ lcl/comctrls.pp
@@ -434,13 +434,13 @@
     procedure SetOptions(const AValue: TCTabControlOptions); virtual;
     procedure AddRemovePageHandle(APage: TCustomPage); virtual;
     procedure CNNotify(var Message: TLMNotify); message CN_NOTIFY;
+    procedure CMTabStopChanged(var Message: TLMessage); message CM_TABSTOPCHANGED;
     class procedure WSRegisterClass; override;
     procedure CreateWnd; override;
     procedure Loaded; override;
     procedure DoChange; virtual;
     procedure InitializeWnd; override;
     procedure Change; virtual;
-    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
     procedure ReadState(Reader: TReader); override;
     function  DialogChar(var Message: TLMKey): boolean; override;
     procedure InternalSetPageIndex(AValue: Integer); // No OnChange
--- lcl/forms.pp.64032
+++ lcl/forms.pp
@@ -1593,6 +1593,7 @@
     // on key down
     procedure DoArrowKey(AControl: TWinControl; var Key: Word; Shift: TShiftState);
     procedure DoTabKey(AControl: TWinControl; var Key: Word; Shift: TShiftState);
+    procedure DoCtrlTabKey(AControl: TWinControl; var Key: Word; Shift: TShiftState);
     // on key up
     procedure DoEscapeKey(AControl: TWinControl; var Key: Word; Shift: TShiftState);
     procedure DoReturnKey(AControl: TWinControl; var Key: Word; Shift: TShiftState);
@@ -1898,6 +1899,7 @@
 {$endif}
 
 uses
+  ComCtrls,
   WSControls, WSForms; // Widgetset uses circle is allowed
 
 var
--- lcl/include/application.inc.64032
+++ lcl/include/application.inc
@@ -1587,6 +1587,7 @@
     // handle navigation key
     DoTabKey(AControl, Key, Shift);
     DoArrowKey(AControl, Key, Shift);
+    DoCtrlTabKey(AControl, Key, Shift);
   end
   else
   begin
@@ -2087,6 +2088,44 @@
     // traverse tabstop controls inside form
     AControl.PerformTab(not (ssShift in Shift));
     Key := VK_UNKNOWN;
+  end;
+end;
+
+procedure TApplication.DoCtrlTabKey(AControl: TWinControl; var Key: Word;
+  Shift: TShiftState);
+var
+  T: TCustomTabControl;
+  FParent: TWinControl;
+begin
+  if (Key = VK_TAB) and ((Shift = [ssCtrl]) or (Shift = [ssCtrl, ssShift])) and
+     AControl.Focused then
+  begin
+    Key := VK_UNKNOWN;
+    // for Ctrl+Tab/Shift+Ctrl+Tab key combi: if self or any parent is a
+    // TCustomTabControl with nboKeyboardTabSwitch option, process it
+    FParent := AControl;
+    while Assigned(FParent) do
+    begin
+      if (FParent is TCustomTabControl) or (FParent is TTabControl) then
+      begin
+        if FParent is TTabControl then
+          T := TTabControlNoteBookStrings(TTabControl(FParent).Tabs).NoteBook
+        else
+          T := TCustomTabControl(FParent);
+        if (nboKeyboardTabSwitch in T.Options) and (T.PageCount > 0) then
+          if Shift = [ssCtrl] then
+          begin
+            T.PageIndex := (T.PageIndex + 1) mod T.PageCount;
+            Break;
+          end
+          else if Shift = [ssCtrl, ssShift] then
+          begin
+            T.PageIndex := (T.PageIndex + T.PageCount - 1) mod T.PageCount;
+            Break;
+          end;
+      end;
+      FParent := FParent.Parent;
+    end;
   end;
 end;
 
--- lcl/include/customnotebook.inc.64032
+++ lcl/include/customnotebook.inc
@@ -768,27 +768,6 @@
 function TCustomTabControl.IsStoredActivePage: boolean;
 begin
   Result:=false;
-end;
-
-procedure TCustomTabControl.KeyDown(var Key: Word; Shift: TShiftState);
-begin
-  if (nboKeyboardTabSwitch in Options) and (Key = VK_TAB) and (PageCount > 0) then 
-  begin
-    if Shift = [ssCtrl] then 
-    begin
-      Key := 0;
-      PageIndex := (PageIndex + 1) mod PageCount;
-      Exit;
-    end
-    else if Shift = [ssCtrl, ssShift] then 
-    begin
-      Key := 0;
-      PageIndex := (PageIndex + PageCount - 1) mod PageCount;
-      Exit;
-    end;
-  end;
-
-  inherited KeyDown(Key, Shift);
 end;
 
 {------------------------------------------------------------------------------
@@ -1102,6 +1081,16 @@
     end;
 end;
 
+
+{------------------------------------------------------------------------------
+  TCustomTabControl CMTabStopChanged
+ ------------------------------------------------------------------------------}
+procedure TCustomTabControl.CMTabStopChanged(var Message: TLMessage);
+begin
+  if HandleAllocated then
+    TWSCustomTabControlClass(WidgetSetClass).SetTabStop(Self, TabStop);
+end;
+
 {------------------------------------------------------------------------------
   procedure TCustomTabControl.ShowCurrentPage
 
@@ -1111,15 +1100,30 @@
 
   function HasFocusedControl(APage: TCustomPage): Boolean;
   var
-    i: Integer;
     lForm: TCustomForm;
+
+    function EnumControls(AControl: TWinControl): Boolean;
+    var
+      i: Integer;
+    begin
+      Result := False;
+      for i := 0 to AControl.ControlCount - 1 do
+      begin
+        if AControl.Controls[i] = lForm.ActiveControl then
+          Result := True
+        else if AControl.Controls[i] is TWinControl then
+          Result := EnumControls(TWinControl(AControl.Controls[i]));
+        if Result then
+          Break;
+      end;
+    end;
+
   begin
     Result := False;
     lForm := GetParentForm(APage);
     if not Assigned(lForm) or not lForm.Visible then Exit;
-    for i := 0 to APage.ControlCount - 1 do
-      if APage.Controls[i] = lForm.ActiveControl then
-        Exit(True);
+    // search APage recursively for focused control
+    Result := EnumControls(APage);
   end;
 
 var
--- lcl/widgetset/wscontrols.pp.64032
+++ lcl/widgetset/wscontrols.pp
@@ -121,6 +121,7 @@
     class procedure SetFont(const AWinControl: TWinControl; const AFont: TFont); virtual;
     class procedure SetPos(const AWinControl: TWinControl; const ALeft, ATop: Integer); virtual;
     class procedure SetSize(const AWinControl: TWinControl; const AWidth, AHeight: Integer); virtual;
+    class procedure SetTabStop(const AWinControl: TWinControl; const AValue: Boolean); virtual;
     class procedure SetText(const AWinControl: TWinControl; const AText: String); virtual;
     class procedure SetCursor(const AWinControl: TWinControl; const ACursor: HCursor); virtual;
     class procedure SetShape(const AWinControl: TWinControl; const AShape: HBITMAP); virtual;
@@ -394,6 +395,11 @@
 begin
 end;
 
+class procedure TWSWinControl.SetTabStop(const AWinControl: TWinControl;
+  const AValue: Boolean);
+begin
+end;
+
 {------------------------------------------------------------------------------
   Method: TWSWinControl.SetLabel
   Params:  AWinControl - the calling object
--- lcl/interfaces/gtk2/gtk2callback.inc.64032
+++ lcl/interfaces/gtk2/gtk2callback.inc
@@ -1783,19 +1783,6 @@
     gtk_list_item_select(PGtkListItem(List^.Data));
   end;
 
-  procedure FixTabControlFocusBehaviour;
-  var
-    Info: PWidgetInfo;
-  begin
-    {gtk_notebook have weird behaviour when clicked.
-     if there's active control on page it'll loose it's
-     focus and trigger OnExit (tab is taking focus).
-     issue #20493}
-    Info := GetWidgetInfo(Widget);
-    if not gtk_widget_is_focus(Widget) then
-      Include(Info^.Flags, wwiTabWidgetFocusCheck);
-  end;
-
 var
   DesignOnlySignal: boolean;
   Msg: TLMContextMenu;
@@ -1852,9 +1839,6 @@
     if Event^.button = 1 then
     begin
       //CaptureMouseForWidget(CaptureWidget,mctGTKIntf);
-      if (TControl(Data) is TCustomTabControl) and
-        not (csDesigning in TControl(Data).ComponentState) then
-          FixTabControlFocusBehaviour;
     end
     else
     // if LCL process LM_CONTEXTMENU then stop the event propagation
--- lcl/interfaces/gtk2/gtk2pagecontrol.inc.64032
+++ lcl/interfaces/gtk2/gtk2pagecontrol.inc
@@ -78,14 +78,14 @@
   end;
 end;
 
-function GtkRestoreFocusFix(AGtkWidget: Pointer): gboolean; cdecl;
-begin
-  Result := AGtkWidget <> nil;
-  if AGtkWidget <> nil then
-  begin
-    GTK_WIDGET_SET_FLAGS(PGtkWidget(AGtkWidget), GTK_CAN_FOCUS);
-    g_idle_remove_by_data(AGtkWidget);
-  end;
+function GtkNotebookPostSwitchPage(AGtkWidget: Pointer): gboolean; cdecl;
+begin
+  Result := False;    // automatically remove from idle list
+  if (AGtkWidget = nil) or not GTK_IS_WIDGET(AGtkWidget) then
+    Exit;
+  // select first child widget if tab control has focus
+  if gtk_widget_has_focus(AGtkWidget) then
+    gtk_widget_child_focus(AGtkWidget, GTK_DIR_DOWN);
 end;
 
 function GtkWSNotebook_AfterSwitchPage(widget: PGtkWidget; {%H-}page: Pgtkwidget; pagenum: integer; data: gPointer): GBoolean; cdecl;
@@ -93,12 +93,8 @@
   Mess: TLMNotify;
   NMHdr: tagNMHDR;
   Info: PWidgetInfo;
-  ACtl: TWinControl;
-  AParentForm: TCustomForm;
-  i: Integer;
   LCLPageIndex: Integer;
-  Pg: TCustomPage;
-  ChildWidget: PGtkWidget;
+  FTabStop, FWasFocused: Boolean;
 begin
   Result := CallBackDefaultReturn;
   // then send the new page
@@ -112,67 +108,21 @@
   Mess.NMHdr := @NMHdr;
   DeliverMessage(Data, Mess);
 
-  // code below is fix for issue #20493
-  Info := GetWidgetInfo(Widget);
-  if wwiTabWidgetFocusCheck in Info^.Flags then
-  begin
-    Exclude(Info^.Flags, wwiTabWidgetFocusCheck);
-
-    if LCLPageIndex = -1 then
-      exit;
-
-    ACtl := TWinControl(Data);
-    AParentForm := GetParentForm(ACtl);
-    if Assigned(AParentForm) then
-    begin
-      // 1st we must find focused control (if any)
-      ACtl := nil;
-      if (LCLPageIndex >= 0) and (LCLPageIndex < TCustomTabControl(Data).PageCount) then
-        Pg := TCustomTabControl(Data).Page[LCLPageIndex]
-      else
-        Pg := nil;
-      if Assigned(Pg) then
-      begin
-        for i := 0 to Pg.ControlCount - 1 do
-        begin
-          if (pg.Controls[i] is TWinControl) and
-            (TWinControl(pg.Controls[i]).Focused) then
-          begin
-            ACtl := TWinControl(pg.Controls[i]);
-            break;
-          end;
-        end;
-      end;
-      if (ACtl = nil) and (Pg <> nil) then
-        ACtl := AParentForm.ActiveControl;
-    end else
-      ACtl := nil;
-
-    if (ACtl <> nil) and (ACtl <> TWinControl(Data)) then
-    begin
-      // DebugLn('ActiveCtl is ',ACtl.ClassName,':',ACtl.Name);
-      // do not focus tab by mouse click if we already have active control
+  if not (TObject(Data) is TNoteBookStringsTabControl) then
+  begin
+    // flags
+    Info := GetWidgetInfo(Widget);
+    FWasFocused := wwiTabWidgetFocusCheck in Info^.Flags;
+    if FWasFocused then
+      Exclude(Info^.Flags, wwiTabWidgetFocusCheck);
+    FTabStop := TWinControl(Data).TabStop;
+    if not FTabStop or (FTabStop and not FWasFocused) then
+      // post switch page function: will select first widget child if
+      // tab control has focus
+      g_idle_add(@GtkNotebookPostSwitchPage, Widget);
+    // restore GTK_CAN_FOCUS based on TabStop
+    if not FTabStop then
       GTK_WIDGET_UNSET_FLAGS(Widget, GTK_CAN_FOCUS);
-      Pg := TCustomTabControl(Data).Page[LCLPageIndex];
-      for i := 0 to Pg.ControlCount - 1 do
-      begin
-        // we must prevent gtkWidget to acquire focus by gtk (eg. GtkButton)
-        if (Pg.Controls[i] is TWinControl) and (Pg.Controls[i] <> ACtl) then
-        begin
-          Info := GetWidgetInfo({%H-}PGtkWidget(TWinControl(Pg.Controls[i]).Handle));
-          if Info <> nil then
-          begin
-            if Info^.CoreWidget <> nil then
-              ChildWidget := Info^.CoreWidget
-            else
-              ChildWidget := Info^.ClientWidget;
-            GTK_WIDGET_UNSET_FLAGS(ChildWidget, GTK_CAN_FOCUS);
-            g_idle_add(@GtkRestoreFocusFix, ChildWidget);
-          end;
-        end;
-      end;
-      g_idle_add(@GtkRestoreFocusFix, Widget);
-    end;
   end;
 end;
 
@@ -181,6 +131,7 @@
   Mess: TLMNotify;
   NMHdr: tagNMHDR;
   IsManual: Boolean;
+  Info: PWidgetInfo;
 begin
   Result := CallBackDefaultReturn;
   EventTrace('switch-page', data);
@@ -192,6 +143,25 @@
     g_object_set_data(PGObject(Widget), LCL_NotebookManualPageSwitchKey, nil);
   if PGtkNotebook(Widget)^.cur_page = nil then // for windows compatibility
     Exit;
+
+  // Note: not applicable to TTabControl (TNoteBookStringsTabControl child)
+  if not (TObject(Data) is TNoteBookStringsTabControl) then
+  begin
+    Info := GetWidgetInfo(Widget);
+    // set temporary flags
+    if gtk_widget_has_focus(Widget) then
+      Include(Info^.Flags, wwiTabWidgetFocusCheck);
+
+    // when switching pages using mouse, gtk2 will automatically set focus
+    // to the tab control, then call gtk_widget_child_focus() to select the
+    // first child widget. if GTK_CAN_FOCUS is disabled (ie. TabStop=False),
+    // focus will be set on the first control, and gtk_widget_child_focus()
+    // will focus the next control.
+
+    // temporary enable GTK_CAN_FOCUS and let tab control have focus
+    GTK_WIDGET_SET_FLAGS(Widget, GTK_CAN_FOCUS);
+    gtk_widget_grab_focus(Widget);
+  end;
 
   // gtkswitchpage is called before the switch
   if not IsManual then
@@ -309,6 +279,8 @@
   Result := HWND(TLCLIntfHandle({%H-}PtrUInt(AWidget)));
   Set_RC_Name(AWinControl, PGtkWidget(AWidget));
   SetCallBacks(PGtkWidget(AWidget), WidgetInfo);
+  if not AWinControl.TabStop then
+    GTK_WIDGET_UNSET_FLAGS(PGtkWidget(AWidget), GTK_CAN_FOCUS);
 end;
 
 class function TGtk2WSCustomTabControl.GetDefaultClientRect(
@@ -599,6 +571,15 @@
     GtkPositionTypeMap[ATabPosition]);
 end;
 
+class procedure TGtk2WSCustomTabControl.SetTabStop(const AWinControl: TWinControl;
+  const AValue: Boolean);
+begin
+  if not AValue then
+    GTK_WIDGET_UNSET_FLAGS({%H-}PGtkWidget(AWinControl.Handle), GTK_CAN_FOCUS)
+  else
+    GTK_WIDGET_SET_FLAGS({%H-}PGtkWidget(AWinControl.Handle), GTK_CAN_FOCUS);
+end;
+
 class procedure TGtk2WSCustomTabControl.ShowTabs(const ATabControl: TCustomTabControl;
   AShowTabs: boolean);
 begin
--- lcl/interfaces/gtk2/gtk2proc.inc.64032
+++ lcl/interfaces/gtk2/gtk2proc.inc
@@ -2130,7 +2130,8 @@
     if (
         GtkWidgetIsA(TargetWidget, gtk_type_entry) or
         GtkWidgetIsA(TargetWidget, gtk_type_text_view) or
-        GtkWidgetIsA(TargetWidget, gtk_type_tree_view)
+        GtkWidgetIsA(TargetWidget, gtk_type_tree_view) or
+        GtkWidgetIsA(TargetWidget, gtk_type_notebook)
        )
        and
       (gdk_event_get_type(AEvent) = GDK_KEY_PRESS) and
--- lcl/interfaces/gtk2/gtk2wscomctrls.pp.64032
+++ lcl/interfaces/gtk2/gtk2wscomctrls.pp
@@ -96,6 +96,7 @@
     class function GetTabRect(const ATabControl: TCustomTabControl; const AIndex: Integer): TRect; override;
     class procedure SetPageIndex(const ATabControl: TCustomTabControl; const AIndex: integer); override;
     class procedure SetTabPosition(const ATabControl: TCustomTabControl; const ATabPosition: TTabPosition); override;
+    class procedure SetTabStop(const AWinControl: TWinControl; const AValue: Boolean); override;
     class procedure ShowTabs(const ATabControl: TCustomTabControl; AShowTabs: boolean); override;
     class procedure UpdateProperties(const ATabControl: TCustomTabControl); override;
   end;
--- lcl/interfaces/gtk2/gtk2wsstdctrls.pp.64032
+++ lcl/interfaces/gtk2/gtk2wsstdctrls.pp
@@ -1741,9 +1741,12 @@
     Gtk2WidgetSet.SetCallbackDirect(LM_FOCUS, AButton, AWinControl);
   end;
   
-  // if we are a GtkComboBoxEntry
-  if not GtkWidgetIsA(PGtkWidget(AEntry), GTK_TYPE_ENTRY) then
-    g_signal_connect(Combowidget, 'grab-focus', TGCallback(@GtkComboFocus), AWidgetInfo);
+  // if we are a GtkComboBoxEntry, do not allow dropdown button to have focus
+  if GtkWidgetIsA(PGtkWidget(AEntry), GTK_TYPE_ENTRY) then
+    GTK_WIDGET_UNSET_FLAGS(APrivate^.box, GTK_CAN_FOCUS)
+  else
+    // if we are a GtkComboBox, attach a callback to trigger onEnter/onExit
+    g_signal_connect(APrivate^.box, 'grab-focus', TGCallback(@GtkComboFocus), AWidgetInfo);
 
   AMenu := nil;
   if (APrivate^.popup_widget <> nil)
--- lcl/interfaces/win32/win32pagecontrol.inc.64032
+++ lcl/interfaces/win32/win32pagecontrol.inc
@@ -752,6 +752,13 @@
     RecreateWnd(ATabControl);
 end;
 
+class procedure TWin32WSCustomTabControl.SetTabStop(const ATabControl: TWinControl; const AValue: Boolean);
+begin
+  if not (csDestroying in ATabControl.ComponentState) then
+    if ATabControl.HandleAllocated then
+      RecreateWnd(ATabControl);
+end;
+
 class procedure TWin32WSCustomTabControl.ShowTabs(const ATabControl: TCustomTabControl; AShowTabs: boolean);
 begin
   if ATabControl is TTabControl then
--- lcl/interfaces/win32/win32wscomctrls.pp.64032
+++ lcl/interfaces/win32/win32wscomctrls.pp
@@ -75,6 +75,7 @@
     class procedure SetImageList(const ATabControl: TCustomTabControl; const AImageList: TCustomImageListResolution); override;
     class procedure SetPageIndex(const ATabControl: TCustomTabControl; const AIndex: integer); override;
     class procedure SetTabPosition(const ATabControl: TCustomTabControl; const ATabPosition: TTabPosition); override;
+    class procedure SetTabStop(const ATabControl: TWinControl; const AValue: Boolean); override;
     class procedure ShowTabs(const ATabControl: TCustomTabControl; AShowTabs: boolean); override;
     class procedure UpdateProperties(const ATabControl: TCustomTabControl); override;
   end;

Issue History

Date Modified Username Field Change
2020-10-17 09:56 Joeny Ang New Issue
2020-10-17 09:56 Joeny Ang File Added: tpagecontrol-quirks-fix.patch
2020-10-17 09:56 Joeny Ang File Added: tpagecontrol-quirks-tests.zip
2020-10-20 10:35 Joeny Ang Note Added: 0126421
2020-10-20 10:35 Joeny Ang File Added: tpagecontrol-quirks-fix-v2.patch
2020-10-20 21:03 Juha Manninen Relationship added related to 0020493
2020-10-20 23:56 jamie philbrook Note Added: 0126436
2020-10-21 10:14 Joeny Ang Note Added: 0126439
2020-10-21 10:49 Joeny Ang Note Edited: 0126439 View Revisions
2020-10-21 11:25 Juha Manninen Note Added: 0126441
2020-10-21 12:26 Joeny Ang Note Added: 0126443
2020-10-26 03:58 Joeny Ang Note Added: 0126560
2020-10-26 03:58 Joeny Ang File Added: tpagecontrol-quirks-fix-v4.patch
2020-10-26 05:30 Joeny Ang Note Edited: 0126560 View Revisions