function MultiTouchDemo(dev, screenId, verbose)
% MultiTouchDemo([dev][, screenId=max][, verbose=0]) - A advanced 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 'verbose' to 1, then all the available info about each
% touch point will be displayed close to the touch point. As drawing
% so much text is very slow, the demo will only update touchpoints
% very slowly!
%
% The demo not only tracks and display touches and their location, as
% well as their timing. It also uses info about the shape of the contact,
% visualizing this as aspect ratio of the drawn rectangle. It tries to
% get info about the contacts orientation and draws accordingly. It tries
% to get (or derive) info about touch pressure and modulates the brightness
% of the rectangle accordingly. One property not used, but available would
% be distance for fingers or tools hovering over a touch surface, if that
% surface provides that info.
%
% This demo currently works on Linux + X11 display system, not on Linux + Wayland.
% It also works on MS-Windows 10 and later.
%
% For background info on capabilities and setup see "help TouchInput".
%

% History:
% 01-Oct-2017 mk  Written.
% 03-Aug-2018 mk  Only exit demo on ESC key, not on all keys. Doc fixes.
% 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:
    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)
  baseSize = RectWidth(rect) / 20;

  % No place for you, little mouse cursor:
  HideCursor(w);

  % Create a 5 pixel texture, just to define a shape we can easily scale and rotate:
  finger = Screen('MakeTexture', w, [1; 0.6; 0.4; 0.4; 0.4]);

  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 = {};
    blobmin = inf;

    buttonstate = 0;
    colmap = [ 1, 0, 0; 0, 1, 0; 0, 0, 1; 1, 1, 0; 1, 0, 1; 0, 1, 1; 1, 1, 1];

    % 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 = colmap(mod(id, 7) + 1, :);
          blobcol{id}.mul = 1.0;
          blobcol{id}.x = evt.MappedX;
          blobcol{id}.y = evt.MappedY;
          blobcol{id}.t = evt.Time;
          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!\n');
          Screen('FillRect', w, [1 0 0]);
          Screen('Flip', w);
          Screen('FillRect', w, 0);
          continue;
        end

        if ismember(evt.Type, [2,3,4])
          evt = GetTouchValuators(evt, info);
          if isfield(evt, 'TouchMajor') && isfield(evt, 'TouchMinor')
            % Shape of primary touch ellipse known:
            if evt.TouchMajor > 0 && evt.TouchMinor > 0
              aspect = evt.TouchMajor / evt.TouchMinor;
            else
              aspect = 1;
            end

            blobcol{id}.rect = [0, 0, baseSize, baseSize * aspect];
            if isfield(evt, 'WidthMajor') && isfield(evt, 'WidthMinor')
              % Shape of approach ellipse known: Can use this to approximate
              % pressure if pressure doesn't get reported:
              blobcol{id}.pressure = evt.TouchMajor / evt.WidthMajor;
            else
              blobcol{id}.pressure = 1;
            end
          else
            % Shape unknown, so assume unit shape:
            blobcol{id}.rect = [0 0  baseSize baseSize];
            blobcol{id}.pressure = 1;
          end

          if isfield(evt, 'Orientation')
            blobcol{id}.orientation = evt.Orientation;
          else
            blobcol{id}.orientation = 0;
          end

          if isfield(evt, 'Pressure')
            blobcol{id}.pressure = evt.Pressure;
          end
        end

        if verbose
          if IsOctave
            blobcol{id}.text = disp(evt);
          else
            blobcol{id}.text = evalc('disp(evt)');
          end
        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('DrawTexture', w, finger, [], CenterRectOnPointd(blobcol{i}.rect * blobcol{i}.mul, blobcol{i}.x, blobcol{i}.y), ...
                 blobcol{i}.orientation, [], [], blobcol{i}.col * blobcol{i}.pressure);
          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:
            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]);
            if verbose
              DrawFormattedText(w, blobcol{i}.text, blobcol{i}.x, blobcol{i}.y + 30, [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);

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

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