Any comment on this task ("click" or "double click")?

Hello,

I explored the possibility of making my program distinguish between a ‘click’ and a ‘double-click.’ For example, if the user presses the ‘i’ key only once, the program does ‘this’ (p1), and if the user presses ‘i’ repeatedly in quick succession, the program does ‘that’ (p2). Note that the program must wait a bit to see if another “i” comes after first receiving one. I ended up with this task:

task body task_react1_t is
t : constant ada.real_time.time := ada.real_time.clock;
i : integer := 0;
p1b,p2b : Procedure_Access;
begin
while i < 2 and ada.real_time.clock < t + dclick1 loop
select
accept key1(p1,p2 : Procedure_Access) do
i := @ + 1;
p1b := p1;
p2b := p2;
end;
else
null;
end select;
end loop;
– Conclusion:
case i is
when 1 => p1b.all;
when 2 => p2b.all;
when others => raise error with "In task_react1_t, i: " & i’Image;
end case;
abort task_react1_t;
end task_react1_t;

I am just smart enough to understand that I am not so smart. Hence, some comments would be nice.

reinert

@reinert, could you edit your code to insert three backticks (“```”) before and after it? that will retain the formatting. As it is, it’s hard to see what’s going on.

I think you’re starting a new instance of the task every time a first-key in a potential key-sequence is pressed. Most people would prefer to have a permanent task, with a loop inside it.

You don’t need to abort the task at the end, just let it drop through.

Here I try you first advice:

   task body task_react1_t is
     t : constant ada.real_time.time := ada.real_time.clock;
     i : integer := 0;
     p1b,p2b : Procedure_Access;
   begin
     while i < 2 and ada.real_time.clock < t + dclick1 loop
       select
          accept key1(p1,p2 : Procedure_Access) do
             i   := @ + 1;
             p1b := p1;
             p2b := p2;
          end;
       else
          null;
       end select;
     end loop;
-- Conclusion:
     case i is
       when 1 => p1b.all;
       when 2 => p2b.all;
       when others => raise error with "In task_react1_t, i: " & i'Image;
     end case;
     abort task_react1_t;
   end task_react1_t;

You have resolved one of my frustrations :slight_smile:

I’ll post an update after first trying out.

reinert

An alternative to the rendezvous option could be to use a protected object.

The specification would be something along the lines of:

with Ada.Real_Time.Timing_Events;
use Ada.Real_Time.Timing_Events;

package Keyboard_Monitors is
   
   type Typing_Pattern is (Once, Twice);

   protected type Keyboard_Monitor is
      procedure Key_Pressed;
      entry Completed(TP: out Typing_Pattern);
   private      
      procedure Event_Handler(Event: in out Timing_Event);
      Event: Timing_Event;
      First: Boolean := True;
      OK: Boolean := False;
      Outcome: Typing_Pattern := Once;
   end Keyboard_Monitor;
end Keyboard_Monitors;

with an implementation of:

with Ada.Real_Time;
use Ada.Real_Time; 

package body Keyboard_Monitors is
   
   Double_Click_Deadline: constant Time_Span := Milliseconds(1_000);

   protected body Keyboard_Monitor is
      
      procedure Key_Pressed is
         Cancelled: Boolean := False;
      begin
         if First then
            Cancel_Handler(Event, Cancelled);
            Outcome := Twice;
            OK := True;
         else
            First := True;
            Set_Handler(Event, Double_Click_Deadline, Event_Handler'Access);
         end if;
      end Key_Pressed;
      
      procedure Reset is
      begin
         First := False;
         OK := False;
         Outcome := Once;
      end Reset;
            
      entry Completed(TP: out Typing_Pattern) when OK is
      begin
         TP := Outcome;
         Reset;
      end Completed;
      
      procedure Event_Handler(Event: in out Timing_Event) is
      begin
         Outcome := Once;
         OK := True;
      end Event_Handler;                              
   end Keyboard_Monitor;
end Keyboard_Monitors;

and then in the client task (perhaps the main task)

with Keyboard_Monitors; use Keyboard_Monitors;

procedure keyboard_action is
   KM: Keyboard_Monitor;
   TP: Typing_Pattern;
begin
   loop
      KM.Completed(TP);
      case TP is
         when Once => null; -- i.e. p1b.all
         when Twice => null; -- i.e. p2b.all         
      end case;
   end loop;
end keyboard_action;

A bit more verbose arguably, but avoids the spin-loop and the need to create a task for a typing monitoring session.

Disclaimer: Haven’t tested that so not entirely convinced it’s race-free but assuming tasks blocked on a barrier are given precedence over the timing-event handler (which I believe is the case) it should be.

Just an observation that if re-entrant entries (and accept statements within the abortable section of the ATC) were somehow allowed this could be implemented somewhat more elegantly using a rendezvous + ATC pattern:

with Ada.Real_Time; use Ada.Real_Time;

procedure Typing_Counter is
         
   task T1 is
      entry S1;
   end T1;
   
   task body T1 is
      Double_Click_Deadline: constant Time_Span := Milliseconds(1_000);
   begin
      loop
         select
            accept S1 do
               select
                  delay until Clock + Double_Click_Deadline;
                  -- action for Once
               then abort
                  accept S1 do
                     -- action for Twice
                     null;
                  end S1;
               end select;
            end S1;
         else
            null;
         end select;
      end loop;
   end T1;
begin
   null;
end Typing_Counter;

(which of-course doesn’t compile :slightly_smiling_face:)

I think the state machine implemented in the PO is much better (especially since - at first glance - it can be implemented in Ravenscar). I might be inclined to name the states in an enumeration, especially if the SM got more complicated (triple-press?!)

That would make more sense. The pattern enum can be extended to
type Typing_Pattern is (None, Once, Twice, Thrice) -- or more

and a current state variable introduced in the private part
Current: Typing_Pattern := None so that Key_Pressed now becomes:

procedure Key_Pressed is
      Cancelled: Boolean := False;
begin
   if Current = Typing_Pattern'Last then
      Current := Typing_Pattern'First;
   else
      Current := Typing_Pattern'Succ(Current);
   end if;
   
   Cancel_Handler(Event, Cancelled); -- Safe even when event is not set
   
   if Current = Cutoff then
      Outcome := Current;
      OK := True;
   else
      Set_Handler(Event, Double_Click_Deadline, Event_Handler'Access);           
   end if;
end Key_Pressed;

where Cutoff can be passed as a discriminant on the PO.

I would like the alternatives are only “once” or “twice” (i.e. only two alternatives). This means that if there is a double-click, the program does not need to wait more to see if there soon comes another (third) click.

reinert

That’s handled in the test if Current = Cutoff then - where Cutoff would be Twice - I think.

Many SMs are a lot more complicated than this, so (IMO) simple is better.

Just a cosmetic modification of the original version (shorter):

  task body task_react1_t is
     t : constant ada.real_time.time := ada.real_time.clock;
     i : integer := 0;
     p : Procedure_Access;
   begin
     while i < 2 and ada.real_time.clock < t + dclick1 loop
       select
          accept key1(p1,p2 : Procedure_Access) do
             i   := @ + 1;
             p := (if i = 1 then p1 else p2);
          end;
       else
          null;
       end select;
     end loop;
     p.all;
     abort task_react1_t;
   end task_react1_t;

Another version using protected object (pause1_t):

   protected body pause1_t is
      entry wait1 when ok is
      begin
         null;
      end wait1;

      procedure halt1 is
      begin
         ok := false;
      end halt1;

      procedure resume1 is
      begin
         ok := true;
      end resume1;
   end pause1_t;

react_pause1 : pause1_t;

   task body task_react1_t is
     t : ada.real_time.time := ada.real_time.clock;
     i : integer := 0;
     p : Procedure_Access;
   begin
     loop
        react_pause1.wait1;
        select
           accept init1 do
              t := ada.real_time.clock;
              i := 0;
           end;
        or
           accept key1(p1,p2 : Procedure_Access) do
              i := @ + 1;
              p := (if i = 1 then p1 else p2);
           end;
        or delay 0.05;
        end select;
        if (ada.real_time.clock > t + dclick1 and i = 1) or i > 1 then
           p.all;
           react_pause1.halt1;
           i := 0;
        end if;
     end loop;
   end task_react1_t;