@@ -72,12 +72,18 @@ public sealed class AppWindow : Gtk.Window {
7272
7373 private uint timer_id = 0 ;
7474 private uint progress_timer_id = 0 ;
75+ private uint radio_vfo_anim_id = 0 ;
7576 private int64 last_refresh_time = 0 ;
77+ private int64 radio_vfo_anim_started_at = 0 ;
7678
7779 private uint current_ticks = 0 ;
7880 private bool update_paused = false ;
7981 private ArrayList<Adw . ViewStackPage > band_pages;
8082 private MapView map_view;
83+ private bool has_displayed_radio_vfo = false ;
84+ private int displayed_radio_vfo_khz = 0 ;
85+ private int radio_vfo_anim_start_khz = 0 ;
86+ private int radio_vfo_anim_target_khz = 0 ;
8187
8288 private ulong program_select_handler = 0 ;
8389
@@ -118,7 +124,7 @@ public sealed class AppWindow : Gtk.Window {
118124 search_entry. search_changed. connect (() = > {
119125 Application . current_search_text = search_entry. text;
120126
121- map_view . bounce_filter ();
127+ bounce_map_filter_if_ready ();
122128 foreach (var page in band_pages) {
123129 var band_view = page. get_child () as BandView ;
124130 band_view. bounce_filter ();
@@ -140,7 +146,7 @@ public sealed class AppWindow : Gtk.Window {
140146
141147 Application . current_mode_filter = mode;
142148
143- map_view . bounce_filter ();
149+ bounce_map_filter_if_ready ();
144150
145151 foreach (var page in band_pages) {
146152 var band_view = page. get_child () as BandView ;
@@ -156,7 +162,10 @@ public sealed class AppWindow : Gtk.Window {
156162
157163 Application . spot_repo. busy_changed. connect ((busy) = > {
158164 loading_spinner. visible = busy;
159- program_select. disconnect (program_select_handler);
165+ if (busy && (program_select_handler != 0 )) {
166+ program_select. disconnect (program_select_handler);
167+ program_select_handler = 0 ;
168+ }
160169 });
161170
162171 Application . spot_repo. refreshed. connect ((spots_updated) = > {
@@ -179,8 +188,10 @@ public sealed class AppWindow : Gtk.Window {
179188 }
180189 }
181190 program_select. set_selected (idx);
182- program_select_handler = program_select. notify[" selected" ]. connect (
183- on_program_selected);
191+ if (program_select_handler == 0 ) {
192+ program_select_handler = program_select. notify[" selected" ]. connect (
193+ on_program_selected);
194+ }
184195
185196 last_refresh_time = get_monotonic_time ();
186197 current_ticks = 0 ;
@@ -228,7 +239,7 @@ public sealed class AppWindow : Gtk.Window {
228239 band_stack. notify[" visible-child-name" ]. connect (() = > {
229240 Application . current_band_filter = band_stack. visible_child_name;
230241 update_status_bar ();
231- map_view . bounce_filter ();
242+ bounce_map_filter_if_ready ();
232243 });
233244
234245 var model = search_select. get_model () as Gtk . StringList ;
@@ -292,11 +303,16 @@ public sealed class AppWindow : Gtk.Window {
292303 (uint )total_visible
293304 ). printf ((uint )total_visible);
294305
295- var status_bar_filtered_text = " ; %u filtered" . printf ((uint )filtered_count);
306+ var status_bar_filtered_text = " • %u filtered" . printf ((uint )filtered_count);
296307
297308 status_bar_text. label = " %s%s " . printf (status_bar_spots_text, status_bar_filtered_text);
298309 }
299310
311+ private void bounce_map_filter_if_ready () {
312+ if (map_view != null )
313+ map_view. bounce_filter ();
314+ }
315+
300316 private void initial_update () {
301317 Application . spot_repo. update_spots. begin ((obj, res) = > {
302318 Application . spot_repo. update_spots. end (res);
@@ -337,8 +353,60 @@ public sealed class AppWindow : Gtk.Window {
337353 }
338354 }
339355
356+ private void stop_radio_vfo_animation () {
357+ if (radio_vfo_anim_id != 0 ) {
358+ Source . remove (radio_vfo_anim_id);
359+ radio_vfo_anim_id = 0 ;
360+ }
361+ }
362+
363+ private void set_radio_vfo_label_animated (int freq_khz ) {
364+ if (! has_displayed_radio_vfo) {
365+ displayed_radio_vfo_khz = freq_khz;
366+ has_displayed_radio_vfo = true ;
367+ radio_vfo. label = format_vfo (freq_khz);
368+ return ;
369+ }
370+
371+ stop_radio_vfo_animation ();
372+
373+ radio_vfo_anim_start_khz = displayed_radio_vfo_khz;
374+ radio_vfo_anim_target_khz = freq_khz;
375+ if (radio_vfo_anim_start_khz == radio_vfo_anim_target_khz) {
376+ radio_vfo. label = format_vfo (freq_khz);
377+ return ;
378+ }
379+
380+ radio_vfo_anim_started_at = get_monotonic_time ();
381+ radio_vfo_anim_id = Timeout . add (16 , () = > {
382+ const double DURATION_MS = 160.0 ;
383+ var elapsed_ms = (get_monotonic_time () - radio_vfo_anim_started_at) / 1000.0 ;
384+ var t = elapsed_ms / DURATION_MS ;
385+ if (t > 1.0 )
386+ t = 1.0 ;
387+
388+ // Ease-out interpolation so the value settles smoothly.
389+ var eased_t = 1.0 - ((1.0 - t) * (1.0 - t));
390+ var interpolated = (double )radio_vfo_anim_start_khz +
391+ ((double )(radio_vfo_anim_target_khz - radio_vfo_anim_start_khz) * eased_t);
392+ displayed_radio_vfo_khz = (int )Math . round (interpolated);
393+ radio_vfo. label = format_vfo (displayed_radio_vfo_khz);
394+
395+ if (t >= 1.0 ) {
396+ displayed_radio_vfo_khz = radio_vfo_anim_target_khz;
397+ radio_vfo. label = format_vfo (displayed_radio_vfo_khz);
398+ radio_vfo_anim_id = 0 ;
399+ return Source . REMOVE ;
400+ }
401+
402+ return Source . CONTINUE ;
403+ });
404+ }
405+
340406 private void power_off_radio () {
341407 Application . radio_control. disconnect (). disown ();
408+ stop_radio_vfo_animation ();
409+ has_displayed_radio_vfo = false ;
342410 radio_vfo. label = _(" Radio disconnected" );
343411 radio_mode. visible = false ;
344412 }
@@ -367,16 +435,20 @@ public sealed class AppWindow : Gtk.Window {
367435 Application . radio_control. radio_status. connect ((freq, mode) = > {
368436 if (freq > 0 && mode != 0 ) {
369437 radio_mode. visible = true ;
370- radio_vfo . label = format_vfo (freq);
438+ set_radio_vfo_label_animated (freq);
371439 radio_mode. label = RadioControl . mode_string (mode);
372440 radio_power_button. active = true ;
373441 } else {
442+ stop_radio_vfo_animation ();
443+ has_displayed_radio_vfo = false ;
374444 radio_mode. visible = false ;
375445 radio_power_button. active = false ;
376446 radio_vfo. label = _(" Radio disconnected" );
377447 }
378448 });
379449 } else {
450+ stop_radio_vfo_animation ();
451+ has_displayed_radio_vfo = false ;
380452 radio_mode. visible = false ;
381453 radio_power_button. active = false ;
382454 radio_vfo. label = _(" Radio disconnected" );
@@ -473,7 +545,7 @@ public sealed class AppWindow : Gtk.Window {
473545
474546 Application . current_program_filter = program;
475547
476- map_view . bounce_filter ();
548+ bounce_map_filter_if_ready ();
477549
478550 foreach (var page in band_pages) {
479551 var band_view = page. get_child () as BandView ;
@@ -490,5 +562,7 @@ public sealed class AppWindow : Gtk.Window {
490562
491563 if (progress_timer_id != 0 )
492564 Source . remove (progress_timer_id);
565+
566+ stop_radio_vfo_animation ();
493567 }
494568} /* class AppWindow */
0 commit comments