#!/usr/bin/perl -w

# Copyright 2011, 2012 Kevin Ryde

# This file is part of X11-Protocol-Other.
#
# X11-Protocol-Other is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 3, or (at your option) any
# later version.
#
# X11-Protocol-Other is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with X11-Protocol-Other.  If not, see <http://www.gnu.org/licenses/>.


# Usage: perl damage-duplicate.pl
#        perl damage-duplicate.pl --id 0x120000f
#
# This is an example of duplicating the contents of a window in real-time by
# listening for changes to it with the DAMAGE extension.
#
# A new $window toplevel displays the contents of a $source window.  The key
# feature of the damage extension is that it reports when $source changes.
# Without that a duplicating program like this would have to re-copy every 1
# second or something like that.
#
# The source window can be given as an XID with the "--id" command line
# option, otherwise an X11::Protocol::ChooseWindow is run so you can click
# on a window like xwininfo does.
#
# Details:
#
# In the event_handler() code care is taken not to do anything which reads a
# reply.  This is because reading the reply may also read and process other
# events, which would call event_handler() recursively and possibly
# endlessly.  Any event handler should bear that in mind.  In this program
# the danger would be that if the $source window is changing rapidly and so
# causing a new DamageNotify event to come very soon after each
# DamageSubtract().
#
# The $gc used for copying has "graphics_expose" off, which means any parts
# not available in the source window are cleared to the destination
# background colour.  This happens when the source is overlapped etc by
# other windows.
#
# In the core protocol there's no easy way to get content from $source when
# it's overlapped, since the server generally doesn't keep the contents or
# generate expose events for obscured parts of windows.
#
# If the "Composite" extension is available then it retains content of
# overlapped windows.  CompositeRedirectWindow() in the setups is all that's
# needed to have full $source contents available for the CopyArea()s.
#
# For simplicity the entire source area is copied whenever it changes.  In a
# more sophisticated program the "$parts_region" of changes from the damage
# object could be a clip mask for the CopyArea().  Changes outside the
# duplicated area would then still go back and forward as DamageNotify and
# CopyArea, but the server would see from the clip region no actual drawing
# required.
#
# If $window is bigger than the $source then the excess is cleared.  Some
# care is taken to clear only the excess area, not the whole of $window,
# since the latter way would make it flash to black and then to the copied
# $source.  On a fast screen you might not notice, but on a slow screen or
# if the server is bogged down then such flashing is very unattractive.
#
# Shortcomings:
#
# The created $window is always on the same screen as $source and uses the
# same depth, visual and colormap.  Doing so means a simple CopyArea
# suffices to copy the contents across.
#
# If source and destination were different depth, visual or colormap then
# pixel colour conversions would be required.  If the destination was on a
# different server or different screen then some data transfers with
# GetImage() and PutImage() would be needed as well as pixel conversions.
# (As usual X11::Protocol::Ext::MIT_SHM might do that through shared memory
# if the program is on the same machine as the server.)
#
# Duplicating the root window is specifically disallowed.  The problem is
# that a draw to $window is a change to the root window contents, so
# generates another DamageNotify, which does another draw, etc, in an
# infinite loop.  It might work if attention was paid to what parts of the
# root had changed.  Changes to the part of the root which is unobscured
# parts of $window will be due to the duplicating drawing and so don't
# require any further drawing.
#

BEGIN { require 5 }
use strict;
use Getopt::Long;
use X11::AtomConstants;
use X11::CursorFont;
use X11::Protocol;
use X11::Protocol::WM;

# uncomment this to run the ### lines
#use Smart::Comments;

my $X = X11::Protocol->new (':0');

if (! $X->init_extension('DAMAGE')) {
  print "DAMAGE extension not available on the server\n";
  exit 1;
}

#------------------------------------------------------------------------------
# command line

my $source;       # source window to duplicate
my $verbose = 1;
GetOptions ('id=s'     => \$source,
            'verbose+' => \$verbose)
  or exit 1;

#------------------------------------------------------------------------------
# source window, from command line or chosen

my $popup_time;

if (defined $source) {
  # command line --id=XID
  $source = oct($source);  # oct() for hex 0xA0000F style as well as decimal
} else {
  require X11::Protocol::ChooseWindow;
  print "Click on a window to duplicate ...\n";
  $source = X11::Protocol::ChooseWindow->choose (X => $X);
  print "  got it\n";
}

if ($verbose) {
  printf "Source window %d (0x%X)\n", $source, $source;
}
if ($source == $X->root) {
  print "Cannot duplicate root window\n";
  exit 1;
}

#------------------------------------------------------------------------------

# use the Composite extension, if available, to keep the contents of $source
# if it's overlapped by other windows.
if ($X->init_extension('Composite')) {
  $X->CompositeRedirectWindow ($source, 'Automatic');
}

#------------------------------------------------------------------------------

my %source_geom = $X->GetGeometry($source);
my %source_attr = $X->GetWindowAttributes($source);

# create new output window to show a duplicate of $source
# same depth, visual, colormap
my $window = $X->new_rsrc;
$X->CreateWindow ($window,
                  $X->root,         # parent
                  'InputOutput',    # class
                  $source_geom{'depth'},
                  $source_attr{'visual'},
                  0,0,              # x,y
                  100,100,          # w,h initial size
                  0,                # border
                  colormap         => $source_attr{'colormap'},
                  background_pixel => $X->black_pixel,
                  event_mask       => $X->pack_event_mask('Exposure'),
                 );
X11::Protocol::WM::set_wm_class ($X, $window,
                                 'damage-duplicate', 'DamageDuplicate');
X11::Protocol::WM::set_wm_name ($X, $window, 'Duplicate Window'); # title
X11::Protocol::WM::set_wm_icon_name ($X, $window, 'Duplicate');
X11::Protocol::WM::set_wm_client_machine_from_syshostname ($X, $window);
X11::Protocol::WM::set_net_wm_pid ($X, $window);
X11::Protocol::WM::set_net_wm_user_time($X, $window, $popup_time);
$X->MapWindow ($window);

# select ConfigureNotify from $source, to know when it resizes
$X->ChangeWindowAttributes
  ($source,
   event_mask => $X->pack_event_mask('StructureNotify'));

# the damage object to monitor $source
# creating this gives DamageNotify events
my $damage = $X->new_rsrc;
$X->DamageCreate ($damage, $source, 'NonEmpty');

my $gc = $X->new_rsrc;
$X->CreateGC ($gc, $window,
              subwindow_mode => 'IncludeInferiors',
              # no "graphics exposures", don't want GraphicsExpose events if
              # a part of the $X->CopyArea is obscured
              graphics_exposures => 0);

sub event_handler {
  my (%h) = @_;
  my $name = $h{'name'};
  ### event_handler()
  ### $name
  if ($name eq 'ConfigureNotify') {
    # $source has resized
    ### height: $h{'height'}
    my $width = $h{'width'};    # of $source
    my $height = $h{'height'};
    # clear any excess if $source has shrunk
    $X->ClearArea ($window, $width,0, 0,0);  # to left of $width
    $X->ClearArea ($window, 0,$height, 0,0); # below $height
    # copy any extra if $source has expanded
    $X->CopyArea ($source, $window, $gc,
                  0,0,                       # src x,y
                  $h{'width'},$h{'height'},  # src w,h
                  0,0);                      # dst x,y

  } elsif ($name eq 'DamageNotify') {
    # $source has been drawn into
    my $rect = $h{'geometry'};
    my ($root_x, $root_y, $width, $height) = @$rect;
    ### $rect
    $X->DamageSubtract ($damage, 'None', 'None');
    $X->CopyArea ($source, $window, $gc,
                  0,0,         # src x,y
                  $width,$height,
                  0,0);        # dst x,y

  } elsif ($name eq 'Expose') {
    # our $window revealed, draw it
    $X->CopyArea ($source, $window, $gc,
                  $h{'x'},$h{'y'},           # src x,y
                  $h{'width'},$h{'height'},  # src w,h
                  $h{'x'},$h{'y'});          # dst x,y
  }
}

$X->{'event_handler'} = \&event_handler;
for (;;) {
  $X->handle_input;
}
exit 0;
