View Issue Details

IDProjectCategoryView StatusLast Update
0037942LazarusLCLpublic2021-03-08 11:02
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;

Joeny Ang

2021-01-21 05:12

reporter   ~0128456

Hi, updated to patch against r64403.

- added code to preserve Ctrl-Tab feature of TCustomMemo in Win32 and GTK2.
  Win32: If WantTabs=False, you can enter a Tab char using Ctrl-Tab.
  GTK2: If WantTabs=True, you can tab out of the control using Ctrl-Tab.
- updated HasFocusedControl() in customnotebook.inc due to recent changes

Tested: GTK2, Win32 (Win10, WinXP)
tpagecontrol-quirks-fix-v6.patch (18,587 bytes)   
--- lcl/comctrls.pp.64114
+++ 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.64114
+++ lcl/forms.pp
@@ -1592,6 +1592,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);
@@ -1897,6 +1898,7 @@
 {$endif}
 
 uses
+  ComCtrls,
   WSControls, WSForms; // Widgetset uses circle is allowed
 
 var
--- lcl/include/application.inc.64339
+++ lcl/include/application.inc
@@ -1584,6 +1584,7 @@
     // handle navigation key
     DoTabKey(AControl, Key, Shift);
     DoArrowKey(AControl, Key, Shift);
+    DoCtrlTabKey(AControl, Key, Shift);
   end
   else
   begin
@@ -2084,6 +2085,45 @@
     // 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
+    // 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;
+            Key := VK_UNKNOWN;
+            Break;
+          end
+          else if Shift = [ssCtrl, ssShift] then
+          begin
+            T.PageIndex := (T.PageIndex + T.PageCount - 1) mod T.PageCount;
+            Key := VK_UNKNOWN;
+            Break;
+          end;
+      end;
+      FParent := FParent.Parent;
+    end;
   end;
 end;
 
--- lcl/include/customnotebook.inc.64339
+++ lcl/include/customnotebook.inc
@@ -769,27 +769,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;
 
 {------------------------------------------------------------------------------
@@ -1103,17 +1082,41 @@
     end;
 end;
 
+{------------------------------------------------------------------------------
+  TCustomTabControl CMTabStopChanged
+ ------------------------------------------------------------------------------}
+procedure TCustomTabControl.CMTabStopChanged(var Message: TLMessage);
+begin
+  if HandleAllocated then
+    TWSCustomTabControlClass(WidgetSetClass).SetTabStop(Self, TabStop);
+end;
+
 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 (lForm=nil) or not lForm.Focused then Exit;
-  for i := 0 to APage.ControlCount - 1 do
-    if APage.Controls[i] = lForm.ActiveControl then
-      Exit(True);
+  if (lForm=nil) or not lForm.Visible then Exit;
+  // search APage recursively for focused control
+  Result := EnumControls(APage);
 end;
 
 procedure TCustomTabControl.ShowCurrentPage;
--- lcl/widgetset/wscontrols.pp.64114
+++ 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.64114
+++ lcl/interfaces/gtk2/gtk2callback.inc
@@ -1789,19 +1789,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;
@@ -1858,9 +1845,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.64114
+++ 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.64339
+++ lcl/interfaces/gtk2/gtk2proc.inc
@@ -2164,7 +2164,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.64339
+++ lcl/interfaces/gtk2/gtk2wscomctrls.pp
@@ -97,6 +97,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.64114
+++ 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.64114
+++ 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.64114
+++ 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;
--- lcl/interfaces/win32/win32callback.inc.64339
+++ lcl/interfaces/win32/win32callback.inc
@@ -2000,6 +2000,7 @@
   CharCodeNotEmpty: boolean;
   R: TRect;
   ACtl: TWinControl;
+  MemoProcessCtrlTab: Boolean;
   LMouseEvent: TTRACKMOUSEEVENT;
   MaximizedActiveChild: WINBOOL;
 {$IF NOT DECLARED(WM_DPICHANGED)} // WM_DPICHANGED was added in FPC 3.1.1
@@ -2666,6 +2667,25 @@
 
   if WinProcess then
   begin
+    // TCustomMemo: search for a tab control parent with nboKeyboardTabSwitch
+    // option and do not process Ctrl+Tab keys if found
+    MemoProcessCtrlTab := True;
+    if (Msg=WM_KEYDOWN) and (WParam=VK_TAB) and
+       (GetKeyState(VK_CONTROL) < 0) and (lWinControl is TCustomMemo) then
+    begin
+      ACtl := lWinControl.Parent;
+      while Assigned(ACtl) do
+      begin
+        if (ACtl is TCustomTabControl) and
+          (nboKeyboardTabSwitch in TCustomTabControl(ACtl).Options) then
+        begin
+          MemoProcessCtrlTab := False;
+          Break;
+        end;
+        ACtl := ACtl.Parent;
+      end;
+    end;
+
     if ((Msg=WM_CHAR) and ((WParam=VK_RETURN) or (WPARAM=VK_ESCAPE)) and
        ((lWinControl is TCustomCombobox) or
         ((lWinControl is TCustomEdit) and not (lWinControl is TCustomMemo ))
@@ -2674,7 +2694,8 @@
     then
       // this thing will beep, don't call defaultWindowProc
     else
-      PLMsg^.Result := CallDefaultWindowProc(Window, Msg, WParam, LParam);
+      if MemoProcessCtrlTab then
+        PLMsg^.Result := CallDefaultWindowProc(Window, Msg, WParam, LParam);
 
     case Msg of
       WM_CHAR, WM_KEYDOWN, WM_KEYUP,

Juha Manninen

2021-01-21 19:40

developer   ~0128468

Now unit Forms adds a circular reference to ComCtrls. Is there no way to do it without?

The patch has interesting format with revision number attached to file names:
--- lcl/comctrls.pp.64114
+++ lcl/comctrls.pp
How do you make such a patch? It is a valid format and can be applied with "patch" command without problems.

My expertise is not enough to analyze the functionality. Martin or wp may know better.

Joeny Ang

2021-01-22 10:26

reporter   ~0128481

Hi, another update :)

- finally, no more circular references :)
- added virtual function TWinControl.DoCtrlTab(). This will be called by
  TApplication.DoCtrlTabKey() when Ctrl-Tab/Ctrl-Shift-Tab is pressed.
  What it does is to call its parent's DoCtrlTab() until a tab control
  is found and the keys are processed in the overriden DoCtrlTab().

Regarding the patch format, it is just my way of naming unmodified files, by
appending the revision number. And yes, this works with "patch" command.
tpagecontrol-quirks-fix-v7.patch (19,851 bytes)   
--- lcl/comctrls.pp.64403
+++ lcl/comctrls.pp
@@ -427,6 +427,7 @@
       AWidth: Integer; AReferenceHandle: TLCLHandle);
     procedure SetImageListAsync(Data: PtrInt);
   protected
+    function DoCtrlTab(const AForward: Boolean): Boolean; override;
     procedure DoAutoAdjustLayout(const AMode: TLayoutAdjustmentPolicy;
       const AXProportion, AYProportion: Double); override;
     function GetPageClass: TCustomPageClass; virtual;
@@ -434,13 +435,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.64403
+++ lcl/controls.pp
@@ -2192,6 +2192,7 @@
     procedure KeyUpBeforeInterface(var Key: Word; Shift: TShiftState); virtual;
     procedure KeyUpAfterInterface(var Key: Word; Shift: TShiftState); virtual;
     procedure UTF8KeyPress(var UTF8Key: TUTF8Char); virtual;
+    function DoCtrlTab(const AForward: Boolean): Boolean; virtual;
   protected
     function  FindNextControl(CurrentControl: TWinControl; GoForward,
                               CheckTabStop, CheckParent: Boolean): TWinControl;
--- lcl/forms.pp.64403
+++ lcl/forms.pp
@@ -1592,6 +1592,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);
--- lcl/include/application.inc.64403
+++ lcl/include/application.inc
@@ -1584,6 +1584,7 @@
     // handle navigation key
     DoTabKey(AControl, Key, Shift);
     DoArrowKey(AControl, Key, Shift);
+    DoCtrlTabKey(AControl, Key, Shift);
   end
   else
   begin
@@ -2084,6 +2085,22 @@
     // traverse tabstop controls inside form
     AControl.PerformTab(not (ssShift in Shift));
     Key := VK_UNKNOWN;
+  end;
+end;
+
+type
+  TWinControlAccess = class(TWinControl);
+
+procedure TApplication.DoCtrlTabKey(AControl: TWinControl; var Key: Word;
+  Shift: TShiftState);
+begin
+  if (Key = VK_TAB) and ((Shift = [ssCtrl]) or (Shift = [ssCtrl, ssShift])) and
+     AControl.Focused then
+  begin
+    // for Ctrl+Tab/Shift+Ctrl+Tab key combi: if self or any parent is a
+    // TCustomTabControl with nboKeyboardTabSwitch option, process it
+    if TWinControlAccess(AControl).DoCtrlTab(Shift = [ssCtrl]) then
+      Key := VK_UNKNOWN;
   end;
 end;
 
--- lcl/include/customnotebook.inc.64403
+++ lcl/include/customnotebook.inc
@@ -771,27 +771,6 @@
   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;
-
 {------------------------------------------------------------------------------
   TCustomTabControl GetPageCount
  ------------------------------------------------------------------------------}
@@ -1103,17 +1082,41 @@
     end;
 end;
 
+{------------------------------------------------------------------------------
+  TCustomTabControl CMTabStopChanged
+ ------------------------------------------------------------------------------}
+procedure TCustomTabControl.CMTabStopChanged(var Message: TLMessage);
+begin
+  if HandleAllocated then
+    TWSCustomTabControlClass(WidgetSetClass).SetTabStop(Self, TabStop);
+end;
+
 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 (lForm=nil) or not lForm.Focused then Exit;
-  for i := 0 to APage.ControlCount - 1 do
-    if APage.Controls[i] = lForm.ActiveControl then
-      Exit(True);
+  if (lForm=nil) or not lForm.Visible then Exit;
+  // search APage recursively for focused control
+  Result := EnumControls(APage);
 end;
 
 procedure TCustomTabControl.ShowCurrentPage;
@@ -1202,3 +1205,23 @@
   Application.QueueAsyncCall(@SetImageListAsync, 0);
 end;
 
+function TCustomTabControl.DoCtrlTab(const AForward: Boolean): Boolean;
+var
+  T: TCustomTabControl;
+begin
+  if Self is TTabControl then   // TTabControl
+    T := TTabControlNoteBookStrings(TTabControl(Self).Tabs).NoteBook
+  else
+    T := Self;                  // TPageControl
+  if (nboKeyboardTabSwitch in T.Options) and (T.PageCount > 0) then
+  begin
+    if AForward then
+      T.PageIndex := (T.PageIndex + 1) mod T.PageCount
+    else
+      T.PageIndex := (T.PageIndex + T.PageCount - 1) mod T.PageCount;
+    Result := True;
+  end
+  else
+    Result := inherited;
+end;
+
--- lcl/include/wincontrol.inc.64403
+++ lcl/include/wincontrol.inc
@@ -6085,6 +6085,13 @@
   Application.ControlKeyUp(Self,Key,Shift);
 end;
 
+function TWinControl.DoCtrlTab(const AForward: Boolean): Boolean;
+begin
+  Result := False;
+  if Assigned(Parent) then
+    Result := Parent.DoCtrlTab(AForward);
+end;
+
 {------------------------------------------------------------------------------
   TWinControl CreateParams
 ------------------------------------------------------------------------------}
--- lcl/widgetset/wscontrols.pp.64403
+++ 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.64403
+++ lcl/interfaces/gtk2/gtk2callback.inc
@@ -1789,19 +1789,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;
@@ -1858,9 +1845,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.64403
+++ 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.64403
+++ lcl/interfaces/gtk2/gtk2proc.inc
@@ -2156,7 +2156,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.64403
+++ 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.64403
+++ 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/win32callback.inc.64403
+++ lcl/interfaces/win32/win32callback.inc
@@ -2000,6 +2000,7 @@
   CharCodeNotEmpty: boolean;
   R: TRect;
   ACtl: TWinControl;
+  MemoProcessCtrlTab: Boolean;
   LMouseEvent: TTRACKMOUSEEVENT;
   MaximizedActiveChild: WINBOOL;
 {$IF NOT DECLARED(WM_DPICHANGED)} // WM_DPICHANGED was added in FPC 3.1.1
@@ -2666,6 +2667,25 @@
 
   if WinProcess then
   begin
+    // TCustomMemo: search for a tab control parent with nboKeyboardTabSwitch
+    // option and do not process Ctrl+Tab keys if found
+    MemoProcessCtrlTab := True;
+    if (Msg=WM_KEYDOWN) and (WParam=VK_TAB) and
+       (GetKeyState(VK_CONTROL) < 0) and (lWinControl is TCustomMemo) then
+    begin
+      ACtl := lWinControl.Parent;
+      while Assigned(ACtl) do
+      begin
+        if (ACtl is TCustomTabControl) and
+          (nboKeyboardTabSwitch in TCustomTabControl(ACtl).Options) then
+        begin
+          MemoProcessCtrlTab := False;
+          Break;
+        end;
+        ACtl := ACtl.Parent;
+      end;
+    end;
+
     if ((Msg=WM_CHAR) and ((WParam=VK_RETURN) or (WPARAM=VK_ESCAPE)) and
        ((lWinControl is TCustomCombobox) or
         ((lWinControl is TCustomEdit) and not (lWinControl is TCustomMemo ))
@@ -2674,7 +2694,8 @@
     then
       // this thing will beep, don't call defaultWindowProc
     else
-      PLMsg^.Result := CallDefaultWindowProc(Window, Msg, WParam, LParam);
+      if MemoProcessCtrlTab then
+        PLMsg^.Result := CallDefaultWindowProc(Window, Msg, WParam, LParam);
 
     case Msg of
       WM_CHAR, WM_KEYDOWN, WM_KEYUP,
--- lcl/interfaces/win32/win32pagecontrol.inc.64403
+++ 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.64403
+++ 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;

Joeny Ang

2021-03-08 11:02

reporter   ~0129506

Hi, updated the patch for r64768.
tpagecontrol-quirks-fix-v9.patch (19,851 bytes)   
--- lcl/comctrls.pp.64403
+++ lcl/comctrls.pp
@@ -427,6 +427,7 @@
       AWidth: Integer; AReferenceHandle: TLCLHandle);
     procedure SetImageListAsync(Data: PtrInt);
   protected
+    function DoCtrlTab(const AForward: Boolean): Boolean; override;
     procedure DoAutoAdjustLayout(const AMode: TLayoutAdjustmentPolicy;
       const AXProportion, AYProportion: Double); override;
     function GetPageClass: TCustomPageClass; virtual;
@@ -434,13 +435,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.64403
+++ lcl/controls.pp
@@ -2192,6 +2192,7 @@
     procedure KeyUpBeforeInterface(var Key: Word; Shift: TShiftState); virtual;
     procedure KeyUpAfterInterface(var Key: Word; Shift: TShiftState); virtual;
     procedure UTF8KeyPress(var UTF8Key: TUTF8Char); virtual;
+    function DoCtrlTab(const AForward: Boolean): Boolean; virtual;
   protected
     function  FindNextControl(CurrentControl: TWinControl; GoForward,
                               CheckTabStop, CheckParent: Boolean): TWinControl;
--- lcl/forms.pp.64679
+++ lcl/forms.pp
@@ -1594,6 +1594,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);
--- lcl/include/application.inc.64760
+++ lcl/include/application.inc
@@ -1643,6 +1643,7 @@
     // handle navigation key
     DoTabKey(AControl, Key, Shift);
     DoArrowKey(AControl, Key, Shift);
+    DoCtrlTabKey(AControl, Key, Shift);
   end
   else
   begin
@@ -2143,6 +2144,22 @@
     // traverse tabstop controls inside form
     AControl.PerformTab(not (ssShift in Shift));
     Key := VK_UNKNOWN;
+  end;
+end;
+
+type
+  TWinControlAccess = class(TWinControl);
+
+procedure TApplication.DoCtrlTabKey(AControl: TWinControl; var Key: Word;
+  Shift: TShiftState);
+begin
+  if (Key = VK_TAB) and ((Shift = [ssCtrl]) or (Shift = [ssCtrl, ssShift])) and
+     AControl.Focused then
+  begin
+    // for Ctrl+Tab/Shift+Ctrl+Tab key combi: if self or any parent is a
+    // TCustomTabControl with nboKeyboardTabSwitch option, process it
+    if TWinControlAccess(AControl).DoCtrlTab(Shift = [ssCtrl]) then
+      Key := VK_UNKNOWN;
   end;
 end;
 
--- lcl/include/customnotebook.inc.64403
+++ lcl/include/customnotebook.inc
@@ -771,27 +771,6 @@
   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;
-
 {------------------------------------------------------------------------------
   TCustomTabControl GetPageCount
  ------------------------------------------------------------------------------}
@@ -1103,17 +1082,41 @@
     end;
 end;
 
+{------------------------------------------------------------------------------
+  TCustomTabControl CMTabStopChanged
+ ------------------------------------------------------------------------------}
+procedure TCustomTabControl.CMTabStopChanged(var Message: TLMessage);
+begin
+  if HandleAllocated then
+    TWSCustomTabControlClass(WidgetSetClass).SetTabStop(Self, TabStop);
+end;
+
 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 (lForm=nil) or not lForm.Focused then Exit;
-  for i := 0 to APage.ControlCount - 1 do
-    if APage.Controls[i] = lForm.ActiveControl then
-      Exit(True);
+  if (lForm=nil) or not lForm.Visible then Exit;
+  // search APage recursively for focused control
+  Result := EnumControls(APage);
 end;
 
 procedure TCustomTabControl.ShowCurrentPage;
@@ -1202,3 +1205,23 @@
   Application.QueueAsyncCall(@SetImageListAsync, 0);
 end;
 
+function TCustomTabControl.DoCtrlTab(const AForward: Boolean): Boolean;
+var
+  T: TCustomTabControl;
+begin
+  if Self is TTabControl then   // TTabControl
+    T := TTabControlNoteBookStrings(TTabControl(Self).Tabs).NoteBook
+  else
+    T := Self;                  // TPageControl
+  if (nboKeyboardTabSwitch in T.Options) and (T.PageCount > 0) then
+  begin
+    if AForward then
+      T.PageIndex := (T.PageIndex + 1) mod T.PageCount
+    else
+      T.PageIndex := (T.PageIndex + T.PageCount - 1) mod T.PageCount;
+    Result := True;
+  end
+  else
+    Result := inherited;
+end;
+
--- lcl/include/wincontrol.inc.64403
+++ lcl/include/wincontrol.inc
@@ -6085,6 +6085,13 @@
   Application.ControlKeyUp(Self,Key,Shift);
 end;
 
+function TWinControl.DoCtrlTab(const AForward: Boolean): Boolean;
+begin
+  Result := False;
+  if Assigned(Parent) then
+    Result := Parent.DoCtrlTab(AForward);
+end;
+
 {------------------------------------------------------------------------------
   TWinControl CreateParams
 ------------------------------------------------------------------------------}
--- lcl/widgetset/wscontrols.pp.64403
+++ 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.64403
+++ lcl/interfaces/gtk2/gtk2callback.inc
@@ -1789,19 +1789,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;
@@ -1858,9 +1845,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.64403
+++ 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.64403
+++ lcl/interfaces/gtk2/gtk2proc.inc
@@ -2156,7 +2156,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.64403
+++ 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.64760
+++ lcl/interfaces/gtk2/gtk2wsstdctrls.pp
@@ -1764,9 +1764,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/win32callback.inc.64403
+++ lcl/interfaces/win32/win32callback.inc
@@ -2000,6 +2000,7 @@
   CharCodeNotEmpty: boolean;
   R: TRect;
   ACtl: TWinControl;
+  MemoProcessCtrlTab: Boolean;
   LMouseEvent: TTRACKMOUSEEVENT;
   MaximizedActiveChild: WINBOOL;
 {$IF NOT DECLARED(WM_DPICHANGED)} // WM_DPICHANGED was added in FPC 3.1.1
@@ -2666,6 +2667,25 @@
 
   if WinProcess then
   begin
+    // TCustomMemo: search for a tab control parent with nboKeyboardTabSwitch
+    // option and do not process Ctrl+Tab keys if found
+    MemoProcessCtrlTab := True;
+    if (Msg=WM_KEYDOWN) and (WParam=VK_TAB) and
+       (GetKeyState(VK_CONTROL) < 0) and (lWinControl is TCustomMemo) then
+    begin
+      ACtl := lWinControl.Parent;
+      while Assigned(ACtl) do
+      begin
+        if (ACtl is TCustomTabControl) and
+          (nboKeyboardTabSwitch in TCustomTabControl(ACtl).Options) then
+        begin
+          MemoProcessCtrlTab := False;
+          Break;
+        end;
+        ACtl := ACtl.Parent;
+      end;
+    end;
+
     if ((Msg=WM_CHAR) and ((WParam=VK_RETURN) or (WPARAM=VK_ESCAPE)) and
        ((lWinControl is TCustomCombobox) or
         ((lWinControl is TCustomEdit) and not (lWinControl is TCustomMemo ))
@@ -2674,7 +2694,8 @@
     then
       // this thing will beep, don't call defaultWindowProc
     else
-      PLMsg^.Result := CallDefaultWindowProc(Window, Msg, WParam, LParam);
+      if MemoProcessCtrlTab then
+        PLMsg^.Result := CallDefaultWindowProc(Window, Msg, WParam, LParam);
 
     case Msg of
       WM_CHAR, WM_KEYDOWN, WM_KEYUP,
--- lcl/interfaces/win32/win32pagecontrol.inc.64403
+++ 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.64403
+++ 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
2021-01-21 05:12 Joeny Ang Note Added: 0128456
2021-01-21 05:12 Joeny Ang File Added: tpagecontrol-quirks-fix-v6.patch
2021-01-21 19:40 Juha Manninen Note Added: 0128468
2021-01-22 10:26 Joeny Ang Note Added: 0128481
2021-01-22 10:26 Joeny Ang File Added: tpagecontrol-quirks-fix-v7.patch
2021-03-08 11:02 Joeny Ang Note Added: 0129506
2021-03-08 11:02 Joeny Ang File Added: tpagecontrol-quirks-fix-v9.patch