function MultiTouchMinimalDemo(dev, screenId, verbose)
% MultiTouchMinimalDemo([dev][, screenId=max][, verbose=0]) - A basic demo for multi-touch touchscreens.
%
% Run it. Pressing the ESCape key will stop it.
%
% Touch the screen and watch the nice colorful happy blobs
% sprinkle to life :)
%
% The demo will try to use the first available touchscreen, or if
% there isn't any touchscreen, the first available touchpad. You
% can also select a specific touch device by passing in its 'dev'
% device handle. Use of touchpads usually needs special configuration.
% See "help TouchInput" for more info.
%
% You can select a specific screen to display on - usually the screenId
% of the touch screen display surface - with the optional 'screenId' parameter,
% or it will select the default maximum screenId if omitted.
%
% If you set the optional 'verbose' flag to 1, then the unique touch
% id and time delta in msecs between touch updates for each touchpoint
% will be printed. This slows the processing down somewhat, as 'DrawText'
% is more slow than most other drawing functions.
%
% This demo currently only works on Linux + X11 display system,
% not on Linux + Wayland, not on other operating systems.
%
% For background info on capabilities and setup see "help TouchInput".
%

% History:
% 05-Oct-2017 mk  Written.
% 03-Aug-2018 mk  Only exit demo on ESC key, not on all keys.
% 05-Dec-2020 mk  Add screenId selection parameter and max screenId default.

  % Setup useful PTB defaults:
  PsychDefaultSetup(2);

  if nargin < 1
    dev = [];
  end

  if nargin < 2 || isempty(screenId)
      screenId = max(Screen('Screens'));
  end

  if nargin < 3 || isempty(verbose)
    verbose = 0;
  end

  % If no user-specified 'dev' was given, try to auto-select:
  if isempty(dev)
    % Get first touchscreen:
    dev = min(GetTouchDeviceIndices([], 1));
  end

  if isempty(dev)
    % Get first touchpad if no touchscreen found:
    dev = min(GetTouchDeviceIndices([], 0));
  end

  if isempty(dev) || ~ismember(dev, GetTouchDeviceIndices)
    fprintf('No touch input device found, or invalid dev given. Bye.\n');
    return;
  else
    fprintf('Touch device properties:\n');
    info = GetTouchDeviceInfo(dev);
    disp(info);
  end

  % Open a default onscreen window with black background color and 0-1 color range:
  [w, rect] = PsychImaging('OpenWindow', screenId, 0);

  % Get maximum supported dot diameter for smooth dots:
  [~, maxSmoothPointSize] = Screen('DrawDots', w);

  % Select good diameter for touch point blobs, but no more than what 'DrawDots' supports:
  baseSize = min(RectWidth(rect) / 20, maxSmoothPointSize);

  HideCursor(w);

  try
    % Create and start touch queue for window and device:
    TouchQueueCreate(w, dev);
    TouchQueueStart(dev);

    % Wait for the go!
    KbReleaseWait;

    % blobcol tracks active touch points - and dying ones:
    blobcol = {};
    buttonstate = 0;
    blobmin = inf;

    % Only ESCape allows to exit the demo:
    RestrictKeysForKbCheck(KbName('ESCAPE'));

    % Main loop: Run until keypress:
    while ~KbCheck
      % Process all currently pending touch events:
      while TouchEventAvail(dev)
        % Process next touch event 'evt':
        evt = TouchEventGet(dev, w);

        % Touch blob id - Unique in the session at least as
        % long as the finger stays on the screen:
        id = evt.Keycode;

        % Keep the id's low, so we have to iterate over less blobcol slots
        % to save computation time:
        if isinf(blobmin)
          blobmin = id - 1;
        end
        id = id - blobmin;

        if evt.Type == 0
          % Not a touch point, but a button press or release on a
          % physical (or emulated) button associated with the touch device:
          buttonstate = evt.Pressed;
          continue;
        end

        if evt.Type == 1
          % Not really a touch point, but movement of the
          % simulated mouse cursor, driven by the primary
          % touch-point:
          Screen('DrawDots', w, [evt.MappedX; evt.MappedY], baseSize, [1,1,1], [], 1, 1);
          continue;
        end

        if evt.Type == 2
          % New touch point -> New blob!
          blobcol{id}.col = rand(3, 1);
          blobcol{id}.mul = 1.0;
          blobcol{id}.x = evt.MappedX;
          blobcol{id}.y = evt.MappedY;
          blobcol{id}.t = evt.Time;
          % Track time delta in msecs between touch point updates:
          blobcol{id}.dt = 0;
        end

        if evt.Type == 3
          % Moving touch point -> Moving blob!
          blobcol{id}.x = evt.MappedX;
          blobcol{id}.y = evt.MappedY;
          blobcol{id}.dt = ceil((evt.Time - blobcol{id}.t) * 1000);
          blobcol{id}.t = evt.Time;
        end

        if evt.Type == 4
          % Touch released - finger taken off the screen -> Dying blob!
          blobcol{id}.mul = 0.999;
          blobcol{id}.x = evt.MappedX;
          blobcol{id}.y = evt.MappedY;
        end

        if evt.Type == 5
          % Lost touch data for some reason:
          % Flush screen red for one video refresh cycle.
          fprintf('Ooops - Sequence data loss! 3rd party interference or overload?\n');
          Screen('FillRect', w, [1 0 0]);
          Screen('Flip', w);
          Screen('FillRect', w, 0);
          continue;
        end
      end

      % Now that all touches for this iteration are processed, repaint screen
      % with all live blobs at their new positions, and fade out the dying/orphaned
      % blobs:
      for i=1:length(blobcol)
        if ~isempty(blobcol{i}) && blobcol{i}.mul > 0.1
          % Draw it: .mul defines size of the blob:
          Screen('DrawDots', w, [blobcol{i}.x, blobcol{i}.y], blobcol{i}.mul * baseSize, blobcol{i}.col, [], 1, 1);
          if blobcol{i}.mul < 1
            % An orphaned blob with no finger touching anymore, so slowly fade it out:
            blobcol{i}.mul = blobcol{i}.mul * 0.95;
          else
            % An active touch. Print its unique touch id and dT timestamp delta between updates in msecs:
            if verbose
              Screen('DrawText', w, num2str(i), blobcol{i}.x, blobcol{i}.y, [1 1 0]);
              Screen('DrawText', w, num2str(blobcol{i}.dt), blobcol{i}.x, blobcol{i}.y - 100, [1 1 0]);
            end
          end
        else
          % Below threshold: Kill the blob:
          blobcol{i} = [];
        end
      end

      if buttonstate
        Screen('FrameRect', w, [1, 1, 0], [], 5);
      end

      % Done repainting - Show it:
      Screen('Flip', w);

      % This little bit here will provoke stimulus onset timing failures on
      % Windows if something is not quite right.
      if verbose == 2 && IsWin
        WaitSecs(0.025);
        [~, ~, ~, visualtimingmaybesane] = GetMouse(w)
      end

      % Next touch processing -> redraw -> flip cycle:
    end

    TouchQueueStop(dev);
    TouchQueueRelease(dev);
    RestrictKeysForKbCheck([]);
    ShowCursor(w);
    sca;
  catch
    TouchQueueRelease(dev);
    RestrictKeysForKbCheck([]);
    sca;
    psychrethrow(psychlasterror);
  end
end
