66using FluentFlyoutWPF . Classes ;
77using FluentFlyoutWPF . Classes . Utils ;
88using System . Runtime . InteropServices ;
9+ using System . Diagnostics ;
910using System . Text ;
1011using System . Windows ;
1112using System . Windows . Automation ;
@@ -32,16 +33,12 @@ public partial class TaskbarWindow : Window
3233 private readonly double _scale = 0.9 ;
3334
3435 private IntPtr _trayHandle ;
35- private AutomationElement ? _widgetElement ;
36- private AutomationElement ? _trayElement ;
37- private AutomationElement ? _taskbarFrameElement ;
3836 // reference to main window for flyout functions
3937 private MainWindow ? _mainWindow ;
4038 private int _lastSelectedMonitor = - 1 ;
4139 private IntPtr _lastTaskbarHandle ;
4240 private bool _positionUpdateInProgress ;
4341 private bool _isClosing ;
44- private readonly Dictionary < string , Task > _pendingAutomationTasks = [ ] ;
4542
4643 private GlobalSystemMediaTransportControlsSessionPlaybackStatus ? _lastPlaybackStatus ;
4744 private DispatcherTimer ? _autoHideTimer ;
@@ -360,10 +357,7 @@ private void ResetTaskbarCachesIfHandleChanged(IntPtr taskbarHandle)
360357
361358 _lastTaskbarHandle = taskbarHandle ;
362359 _trayHandle = IntPtr . Zero ;
363- _widgetElement = null ;
364- _trayElement = null ;
365- _taskbarFrameElement = null ;
366- _pendingAutomationTasks . Clear ( ) ;
360+ _xamlElementStates . Clear ( ) ;
367361 }
368362
369363 private void CalculateAndSetPosition ( IntPtr taskbarHandle , IntPtr taskbarWindowHandle , bool isMainTaskbarSelected )
@@ -776,101 +770,121 @@ public void RefreshAppVolumeTooltip()
776770 Widget . RefreshAppVolumeTooltip ( ) ;
777771 }
778772
779- private ( bool , Rect ) GetTaskbarXamlElementRect ( IntPtr taskbarHandle , ref AutomationElement ? elementCache , string elementName )
773+ /// <summary>
774+ /// Per-element cache for taskbar XAML element rects queried via UI Automation.
775+ /// All queries run on a background thread; the UI thread never blocks on them.
776+ /// </summary>
777+ private sealed class XamlElementState
778+ {
779+ public AutomationElement ? Element ;
780+ public Rect Rect = Rect . Empty ;
781+ public bool HasRect ;
782+ public int QueryInFlight ; // 0/1 - Interlocked
783+ public DateTime LastQueryCompleted = DateTime . MinValue ;
784+ public long QueryDurationMs ;
785+ }
786+
787+ private readonly Dictionary < string , XamlElementState > _xamlElementStates = new ( ) ;
788+
789+ /// <summary>
790+ /// Returns the cached bounding rect of a taskbar XAML element, refreshing it in the
791+ /// background when stale. Never blocks the caller; if no rect is available yet the
792+ /// caller falls back to its default positioning. The stale-limit keeps the rect fresh
793+ /// without querying the taskbar's UI Automation tree on every timer tick.
794+ /// </summary>
795+ private ( bool , Rect ) GetTaskbarXamlElementRect ( IntPtr taskbarHandle , string elementName )
780796 {
781797 if ( taskbarHandle == IntPtr . Zero )
782798 return ( false , Rect . Empty ) ;
783799
784- try
800+ if ( ! _xamlElementStates . TryGetValue ( elementName , out var state ) )
801+ _xamlElementStates [ elementName ] = state = new XamlElementState ( ) ;
802+
803+ // reset if monitor changed
804+ if ( _lastSelectedMonitor != SettingsManager . Current . TaskbarWidgetSelectedMonitor )
785805 {
786- // reset if monitor changed
787- if ( _lastSelectedMonitor != SettingsManager . Current . TaskbarWidgetSelectedMonitor )
788- elementCache = null ;
806+ state . HasRect = false ;
807+ state . Element = null ;
808+ }
789809
790- // find widget in XAML
791- if ( elementCache == null )
792- {
793- if ( _pendingAutomationTasks . TryGetValue ( elementName , out var pendingTask ) && ! pendingTask . IsCompleted )
794- return ( false , Rect . Empty ) ;
810+ bool needsRefresh = ! state . HasRect
811+ || DateTime . Now . Subtract ( state . LastQueryCompleted ) > TimeSpan . FromSeconds ( 3 ) ;
795812
796- AutomationElement ? found = null ;
797- var findTask = Task . Run ( ( ) =>
798- {
799- var root = AutomationElement . FromHandle ( taskbarHandle ) ;
800- found = root . FindFirst ( TreeScope . Descendants ,
801- new PropertyCondition ( AutomationElement . AutomationIdProperty , elementName ) ) ;
802- } ) ;
803- _pendingAutomationTasks [ elementName ] = findTask ;
813+ // if the last query took a long time, the taskbar's UIA tree is in a slow state -
814+ // back off proportionally so a creeping walk does not keep consuming a core
815+ long backoffMs = state . QueryDurationMs > 300
816+ ? Math . Min ( state . QueryDurationMs * 10 , 60000 )
817+ : 3000 ;
818+ bool inBackoff = DateTime . Now . Subtract ( state . LastQueryCompleted ) . TotalMilliseconds < backoffMs ;
804819
805- if ( ! findTask . Wait ( 1000 ) )
806- {
807- Logger . Warn ( "Timeout querying taskbar XAML element: " + elementName ) ;
808- return ( false , Rect . Empty ) ;
809- }
820+ if ( inBackoff )
821+ return state . HasRect ? ( true , state . Rect ) : ( false , Rect . Empty ) ;
810822
811- // Propagate any exception from the background thread
812- findTask . GetAwaiter ( ) . GetResult ( ) ;
813- elementCache = found ;
814- }
823+ if ( needsRefresh
824+ && Interlocked . CompareExchange ( ref state . QueryInFlight , 1 , 0 ) == 0 )
825+ {
826+ StartXamlElementQuery ( taskbarHandle , elementName , state ) ;
827+ }
815828
816- if ( elementCache == null ) // widget most likely disabled
817- return ( false , Rect . Empty ) ;
829+ return state . HasRect ? ( true , state . Rect ) : ( false , Rect . Empty ) ;
830+ }
818831
832+ private void StartXamlElementQuery ( IntPtr taskbarHandle , string elementName , XamlElementState state )
833+ {
834+ Task . Run ( ( ) =>
835+ {
836+ var sw = Stopwatch . StartNew ( ) ;
837+ Rect rect = Rect . Empty ;
838+ AutomationElement ? found = null ;
819839 try
820840 {
821- if ( _pendingAutomationTasks . TryGetValue ( elementName , out var pendingTask ) && ! pendingTask . IsCompleted )
841+ var root = AutomationElement . FromHandle ( taskbarHandle ) ;
842+ found = root . FindFirst ( TreeScope . Descendants ,
843+ new PropertyCondition ( AutomationElement . AutomationIdProperty , elementName ) ) ;
844+ if ( found != null )
822845 {
823- elementCache = null ;
824- return ( false , Rect . Empty ) ;
846+ try
847+ {
848+ rect = found . Current . BoundingRectangle ;
849+ }
850+ catch ( ElementNotAvailableException )
851+ {
852+ found = null ;
853+ }
825854 }
855+ }
856+ catch ( Exception ex )
857+ {
858+ Logger . Warn ( ex , "Error querying taskbar XAML element: " + elementName ) ;
859+ }
860+ long durationMs = sw . ElapsedMilliseconds ;
826861
827- var cachedElement = elementCache ;
828- var boundsTask = Task . Run ( ( ) => cachedElement . Current . BoundingRectangle ) ;
829- _pendingAutomationTasks [ elementName ] = boundsTask ;
830-
831- if ( ! boundsTask . Wait ( 500 ) )
862+ // complete the query on the UI thread
863+ Dispatcher . BeginInvoke ( ( ) =>
864+ {
865+ if ( found == null || rect == Rect . Empty )
832866 {
833- Logger . Warn ( "Timeout getting bounds for taskbar XAML element: " + elementName ) ;
834- elementCache = null ;
835- return ( false , Rect . Empty ) ;
867+ // element is gone/disabled; drop the cache and back off before retrying
868+ state . HasRect = false ;
869+ state . Element = null ;
870+ state . LastQueryCompleted = DateTime . Now ;
836871 }
837-
838- Rect elementRect = boundsTask . GetAwaiter ( ) . GetResult ( ) ;
839-
840- if ( elementRect == Rect . Empty ) // widget shown before but most likely disabled now
872+ else
841873 {
842- elementCache = null ; // reset cache
843- return ( false , Rect . Empty ) ;
874+ // no change -> keep existing element reference to avoid stale handles
875+ state . Element = found ;
876+ state . Rect = rect ;
877+ state . HasRect = true ;
878+ state . LastQueryCompleted = DateTime . Now ;
844879 }
845880
846- return ( true , elementRect ) ;
847- }
848- catch ( ElementNotAvailableException )
849- {
850- // element became stale, reset cache
851- Logger . Warn ( "Taskbar XAML element became stale, resetting cache: " + elementName ) ;
852- elementCache = null ;
853- return ( false , Rect . Empty ) ;
854- }
855- }
856- catch ( COMException ex )
857- {
858- Logger . Warn ( ex , "COM error retrieving taskbar XAML element Rect: " + elementName ) ;
859- elementCache = null ; // reset cache on error
860- return ( false , Rect . Empty ) ;
861- }
862- catch ( ElementNotAvailableException )
863- {
864- Logger . Warn ( "Taskbar XAML element not available, resetting cache: " + elementName ) ;
865- elementCache = null ;
866- return ( false , Rect . Empty ) ;
867- }
868- catch ( Exception ex )
869- {
870- Logger . Error ( ex , "Error retrieving taskbar XAML element Rect: " + elementName ) ;
871- elementCache = null ; // reset cache on error
872- return ( false , Rect . Empty ) ;
873- }
881+ state . QueryDurationMs = durationMs ;
882+ Interlocked . Exchange ( ref state . QueryInFlight , 0 ) ;
883+
884+ // apply the refreshed position now that the rect may have changed
885+ UpdatePosition ( ) ;
886+ } , DispatcherPriority . Background ) ;
887+ } ) ;
874888 }
875889
876890 /// <summary>
@@ -881,17 +895,17 @@ public void RefreshAppVolumeTooltip()
881895 /// <see cref="Rect.Empty"/> if not found.</returns>
882896 private ( bool , Rect ) GetTaskbarWidgetRect ( IntPtr taskbarHandle )
883897 {
884- return GetTaskbarXamlElementRect ( taskbarHandle , ref _widgetElement , "WidgetsButton" ) ;
898+ return GetTaskbarXamlElementRect ( taskbarHandle , "WidgetsButton" ) ;
885899 }
886900
887901 private ( bool , Rect ) GetSystemTrayRect ( IntPtr taskbarHandle )
888902 {
889- return GetTaskbarXamlElementRect ( taskbarHandle , ref _trayElement , "SystemTrayIcon" ) ;
903+ return GetTaskbarXamlElementRect ( taskbarHandle , "SystemTrayIcon" ) ;
890904 }
891905
892906 private ( bool , Rect ) GetTaskbarFrameRect ( IntPtr taskbarHandle )
893907 {
894- return GetTaskbarXamlElementRect ( taskbarHandle , ref _taskbarFrameElement , "TaskbarFrame" ) ;
908+ return GetTaskbarXamlElementRect ( taskbarHandle , "TaskbarFrame" ) ;
895909 }
896910
897911 protected override void OnClosed ( EventArgs e )
@@ -900,10 +914,7 @@ protected override void OnClosed(EventArgs e)
900914 _timer . Stop ( ) ;
901915 _autoHideTimer ? . Stop ( ) ;
902916 _autoHideTimer = null ;
903- _widgetElement = null ;
904- _trayElement = null ;
905- _taskbarFrameElement = null ;
906- _pendingAutomationTasks . Clear ( ) ;
917+ _xamlElementStates . Clear ( ) ;
907918 base . OnClosed ( e ) ;
908919 }
909920}
0 commit comments