The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -w

# Copyright 2008, 2009, 2010, 2011 Kevin Ryde

# This file is part of Gtk2-Ex-Xor.
#
# Gtk2-Ex-Xor 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.
#
# Gtk2-Ex-Xor 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 Gtk2-Ex-Xor.  If not, see <http://www.gnu.org/licenses/>.


use strict;
use warnings;
use Test::More;

use lib 't';
use MyTestHelpers;
BEGIN { MyTestHelpers::nowarnings() }

require Gtk2::Ex::CrossHair;

require Gtk2;
Gtk2->init_check
  or plan skip_all => 'due to no DISPLAY available';
plan tests => 42;

my $want_version = 22;
is ($Gtk2::Ex::CrossHair::VERSION, $want_version, 'VERSION variable');
is (Gtk2::Ex::CrossHair->VERSION,  $want_version, 'VERSION class method');
{ ok (eval { Gtk2::Ex::CrossHair->VERSION($want_version); 1 },
      "VERSION class check $want_version");
  my $check_version = $want_version + 1000;
  ok (! eval { Gtk2::Ex::CrossHair->VERSION($check_version); 1 },
      "VERSION class check $check_version");
}
{
  my $cross = Gtk2::Ex::CrossHair->new;
  is ($cross->VERSION, $want_version, 'VERSION object method');
  ok (eval { $cross->VERSION($want_version); 1 },
      "VERSION object check $want_version");
  my $check_version = $want_version + 1000;
  ok (! eval { $cross->VERSION($check_version); 1 },
      "VERSION object check $check_version");
}

# return an arrayref
sub leftover_fields {
  my ($widget) = @_;
  my @leftover = grep /Gtk2::Ex::CrossHair/, keys %$widget;

  #   if (my $connected = MyTestHelpers::any_signal_connections ($widget)) {
  #     push @leftover, "signal $connected";
  #   }

  if (@leftover) {
    my %leftover;
    @leftover{@leftover} = @{$widget}{@leftover}; # hash slice
    diag "leftover fields: ", keys %leftover;
  }
  return \@leftover;
}

sub show_wait {
  my ($widget) = @_;
  my ($t_id, $s_id);
  $t_id = Glib::Timeout->add (10_000, # 10 seconds
                              sub {
                                diag "Timeout waiting for map event";
                                exit 1;
                              });
  $s_id = $widget->signal_connect (map_event => sub {
                                     Gtk2->main_quit;
                                     return 0; # propagate event
                                   });
  $widget->show;
  Gtk2->main;
  $widget->signal_handler_disconnect ($s_id);
  Glib::Source->remove ($t_id);
}


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

require Gtk2;
MyTestHelpers::glib_gtk_versions();

# setting 'widget' notifies 'widgets' too
{
  my $widget = Gtk2::Window->new ('toplevel');
  my $cross = Gtk2::Ex::CrossHair->new;

  my $seen_widget = 0;
  my $seen_widgets = 0;
  $cross->signal_connect ('notify::widget'  => sub { $seen_widget++; });
  $cross->signal_connect ('notify::widgets' => sub { $seen_widgets++; });

  $cross->set (widget => $widget);
  is ($seen_widget,  1, 'notify widget');
  is ($seen_widgets, 1, 'notify widgets');
  $widget->destroy;
}

# setting 'widgets' notifies 'widget' too
{
  my $widget = Gtk2::Window->new ('toplevel');
  my $cross = Gtk2::Ex::CrossHair->new;

  my $seen_widget = 0;
  my $seen_widgets = 0;
  $cross->signal_connect ('notify::widget'  => sub { $seen_widget++; });
  $cross->signal_connect ('notify::widgets' => sub { $seen_widgets++; });

  $cross->set (widgets => [$widget]);
  is ($seen_widget,  1, 'notify widget');
  is ($seen_widgets, 1, 'notify widgets');
  $widget->destroy;
}


# destroyed when weakened empty
{
  my $cross = Gtk2::Ex::CrossHair->new;
  my $weak_cross = $cross;
  require Scalar::Util;
  Scalar::Util::weaken ($weak_cross);
  undef $cross;
  is ($weak_cross, undef, 'weaken empty - destroyed');
  if (defined &explain) { diag explain($weak_cross); }
  if ($weak_cross) { MyTestHelpers::findrefs ($weak_cross); }
}

# destroyed when weakened on unrealized
{
  my $widget = Gtk2::Window->new ('toplevel');
  my $cross = Gtk2::Ex::CrossHair->new (widget => $widget);
  my $weak_cross = $cross;
  require Scalar::Util;
  Scalar::Util::weaken ($weak_cross);
  undef $cross;
  MyTestHelpers::main_iterations();
  is ($weak_cross, undef, 'weaken unrealized - destroyed');
  if (defined &explain) {
    diag explain($widget);
    diag explain($weak_cross);
  }
  if ($weak_cross) {
    MyTestHelpers::findrefs ($weak_cross);
  }
  is_deeply (leftover_fields($widget), [],
             'weaken unrealized - no CrossHair data left behind');
  $widget->destroy;
}

# destroyed when weakened on realized
{
  my $widget = Gtk2::Window->new ('toplevel');
  $widget->realize;
  my $cross = Gtk2::Ex::CrossHair->new (widget => $widget);
  my $weak_cross = $cross;
  Scalar::Util::weaken ($weak_cross);
  $cross = undef;
  is ($weak_cross, undef, 'weaken realized - destroyed');
  is_deeply (leftover_fields($widget), [],
             'weaken realized - no CrossHair data left behind');
  $widget->destroy;
}

# destroyed when weakened on active
SKIP: {
  Gtk2::Gdk::Display->can('warp_pointer')
      or skip 'no display->warp_pointer(), per Gtk before 2.8', 3;

  my $widget = Gtk2::Window->new ('toplevel');
  $widget->set_size_request (100, 100);
  show_wait ($widget);

  # temporary warp to have mouse pointer within $widget
  my $display = $widget->get_display;
  my ($screen,$x,$y) = $display->get_pointer;
  my ($widget_x,$widget_y) = $widget->window->get_origin;
  $display->warp_pointer($widget->get_screen,$widget_x+50,$widget_y+50);

  is_deeply (leftover_fields($widget), [],
             'weaken active - initially no CrossHair data');

  my $cross = Gtk2::Ex::CrossHair->new (widget => $widget);
  $cross->start;
  # sync and iterate to make the cross draw and use its gc
  $display->sync;
  MyTestHelpers::main_iterations();

  my $weak_cross = $cross;
  Scalar::Util::weaken ($weak_cross);
  $cross = undef;
  MyTestHelpers::main_iterations();
  is ($weak_cross, undef, 'weaken active - destroyed');
  if ($weak_cross) {
    if (defined &explain) { diag explain($weak_cross); }
    MyTestHelpers::findrefs ($weak_cross);
  }
  is_deeply (leftover_fields($widget), [],
             'weaken active - no CrossHair data left behind');

  $widget->destroy;
  $display->warp_pointer($screen,$x,$y);
}

# start() emits "notify::active"
{
  my $widget = Gtk2::Window->new ('toplevel');
  $widget->realize;
  my $cross = Gtk2::Ex::CrossHair->new (widget => $widget);
  my $seen_notify = 0;
  $cross->signal_connect ('notify::active' => sub { $seen_notify = 1; });
  $cross->start;
  is ($seen_notify, 1, 'start() emits notify::active');
  $widget->destroy;
}

# end() emits "notify::active"
{
  my $widget = Gtk2::Window->new ('toplevel');
  $widget->realize;
  my $cross = Gtk2::Ex::CrossHair->new (widget => $widget);
  $cross->start;
  my $seen_notify = 0;
  $cross->signal_connect ('notify::active' => sub { $seen_notify = 1; });
  $cross->end;
  is ($seen_notify, 1, 'end() emits notify::active');
    $widget->destroy;
  }

# leftovers on changing widget, and switching to a widget without a common
# ancestor with the previous
SKIP: {
  Gtk2::Gdk::Display->can('warp_pointer')
      or skip 'no display->warp_pointer(), per Gtk before 2.8', 2;

  my $widget = Gtk2::Window->new ('toplevel');
  my $widget2 = Gtk2::Window->new ('toplevel');
  $widget->set_size_request (100, 100);
  show_wait ($widget);
  show_wait ($widget2);

  # temporary warp to have mouse pointer within $widget
  my $display = $widget->get_display;
  my ($screen,$x,$y) = $display->get_pointer;
  my ($widget_x,$widget_y) = $widget->window->get_origin;
  $display->warp_pointer($widget->get_screen,$widget_x+50,$widget_y+50);

  my $cross = Gtk2::Ex::CrossHair->new (widget => $widget);
  $cross->start;
  # sync and iterate to make the cross draw and use its gc
  $display->sync;
  MyTestHelpers::main_iterations();

  $cross->set (widget => $widget2);
  ($widget_x,$widget_y) = $widget2->window->get_origin;
  $display->warp_pointer($widget2->get_screen,$widget_x+50,$widget_y+50);
  $display->sync;
  MyTestHelpers::main_iterations();

  # if (defined &explain) {
  #   diag explain($widget);
  # }
  is_deeply (leftover_fields($widget), [],
             'change widget - no CrossHair data left behind');

  $cross->set (widgets => []);
  is_deeply (leftover_fields($widget2), [],
             'change to no widgets - no CrossHair data left behind');

  $widget->destroy;
  $widget2->destroy;
  $display->warp_pointer($screen,$x,$y);
}

#------------------------------------------------------------------------------
# foreground properties

# return true if two Glib::Boxed objects $b1 and $b2 point to the same
# underlying C object
{
  my $n = 0;
  sub color_parts {
    my ($color) = @_;
    if (Scalar::Util::blessed($color)) {
      return $color->red .','. $color->blue .','. $color->green;
    } else {
      return 'not-a-color-object'.$n++;
    }
  }
}

{
  my $crosshair = Gtk2::Ex::CrossHair->new;
  my %notifies;
  $crosshair->signal_connect (notify => sub {
                            my ($crosshair, $pspec) = @_;
                            my $pname = $pspec->get_name;
                            $notifies{$pname} = 1;
                          });

  # claimed defaults
  is ($crosshair->get('foreground'), undef, 'foreground - default undef');
  is ($crosshair->get('foreground-name'), undef, 'foreground-name - default undef');
  is ($crosshair->get('foreground-gdk'), undef,
      'foreground-gdk - default undef');

  # string
  %notifies = ();
  $crosshair->set (foreground => 'white');
  is ($crosshair->get('foreground'), 'white');
  is ($crosshair->get('foreground-name'), 'white');
  is (color_parts ($crosshair->get('foreground-gdk')),
      color_parts (Gtk2::Gdk::Color->new(65535,65535,65535)));
  is_deeply (\%notifies, {foreground=>1,foreground_name=>1,foreground_gdk=>1},
             'foreground string notifies');

  # object
  my $red = Gtk2::Gdk::Color->new (65535,0,0);
  %notifies = ();
  $crosshair->set (foreground => $red);
  is (color_parts ($crosshair->get('foreground')),
      color_parts ($red));
  is ($crosshair->get('foreground-name'), '#ffff00000000');
  # boxed objects not equal
  is (color_parts ($crosshair->get('foreground-gdk')),
      color_parts ($red));
  is_deeply (\%notifies, {foreground=>1,foreground_name=>1,foreground_gdk=>1},
             'foreground object notifies');

  # foreground-name string
  %notifies = ();
  $crosshair->set (foreground_name => 'black');
  is ($crosshair->get('foreground'), 'black');
  is ($crosshair->get('foreground-name'), 'black');
  is (color_parts ($crosshair->get('foreground-gdk')),
      color_parts (Gtk2::Gdk::Color->new (0,0,0)));
  is_deeply (\%notifies, {foreground=>1,foreground_name=>1,foreground_gdk=>1},
             'foreground-name notifies');

  # foreground-gdk object
  my $green = Gtk2::Gdk::Color->new (0,65535,0);
  %notifies = ();
  $crosshair->set (foreground_gdk => $green);
  is (color_parts ($crosshair->get('foreground')),
      color_parts ($green));
  is ($crosshair->get('foreground-name'), '#0000ffff0000');
  # boxed objects not equal
  is (color_parts ($crosshair->get('foreground-gdk')),
      color_parts ($green));
  is_deeply (\%notifies, {foreground=>1,foreground_name=>1,foreground_gdk=>1},
             'foreground-gdk notifies');
}

exit 0;