The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Padre::Plugin::SpellCheck::Dialog;
BEGIN {
  $Padre::Plugin::SpellCheck::Dialog::VERSION = '1.21';
}

# ABSTRACT: Spell check dialog for Padre

use warnings;
use strict;

use Class::XSAccessor accessors => {
	_autoreplace => '_autoreplace', # list of automatic replaces
	_engine      => '_engine',      # pps:engine object
	_error       => '_errorpos',    # first error spotted [ $word, $pos ]
	_label       => '_label',       # label hosting the misspelled word
	_list        => '_list',        # listbox listing the suggestions
	_offset      => '_offset',      # offset of _text within the editor
	_plugin      => '_plugin',      # reference to spellcheck plugin
	_sizer       => '_sizer',       # window sizer
	_text        => '_text',        # text being spellchecked
};

use Padre::Current;
use Padre::Wx   ();
use Padre::Util ('_T');
use Encode;

use base 'Wx::Dialog';


# -- constructor

sub new {
	my ( $class, %params ) = @_;

	# create object
	my $config = $params{plugin}->config;
	my $self   = $class->SUPER::new(
		Padre::Current->main,
		-1,
		sprintf( _T('Spelling (%s)'), $config->{dictionary} ),
		Wx::wxDefaultPosition,
		Wx::wxDefaultSize,
		Wx::wxDEFAULT_FRAME_STYLE | Wx::wxTAB_TRAVERSAL,
	);
	$self->SetIcon( Wx::GetWxPerlIcon() );
	$self->_error( $params{error} );
	$self->_engine( $params{engine} );
	$self->_offset( $params{offset} );
	$self->_text( $params{text} );
	$self->_plugin( $params{plugin} );
	$self->_autoreplace( {} );

	# create dialog
	$self->_create;
	$self->_update;

	return $self;
}

# -- public methods

# -- gui handlers

#
# $self->_on_butclose_clicked;
#
# handler called when the close button has been clicked.
#
sub _on_butclose_clicked {
	my $self = shift;
	$self->Destroy;
}

#
# $self->_on_butignore_all_clicked;
#
# handler called when the ignore all button has been clicked.
#
sub _on_butignore_all_clicked {
	my ($self) = @_;

	my $word = $self->_error->[0];
	$self->_engine->ignore($word);
	$self->_on_butignore_clicked;
}

#
# $self->_on_butignore_clicked;
#
# handler called when the ignore button has been clicked.
#
sub _on_butignore_clicked {
	my ($self) = @_;

	# remove the beginning of the text, up to after current error
	my $error = $self->_error;
	my ( $word, $pos ) = @$error;
	$pos += length $word;
	my $text = substr $self->_text, $pos;
	$self->_text($text);
	my $offset = $self->_offset + $pos;
	$self->_offset($offset);

	# FIXME: as soon as STC issue is resolved:
	# Include UTF8 characters from ignored word
	# to overall count of UTF8 characters
	# so we can set proper selections
	$self->_engine->_count_utf_chars($word);

	# try to find next error
	$self->_next;
}

#
# $self->_on_butreplace_all_clicked;
#
# handler called when the replace all button has been clicked.
#
sub _on_butreplace_all_clicked {
	my ($self) = @_;

	# get replacing word
	my $list = $self->_list;
	my $id = $list->GetNextItem( -1, Wx::wxLIST_NEXT_ALL, Wx::wxLIST_STATE_SELECTED );
	return if $id == -1;
	my $new = $list->GetItem($id)->GetText;

	# store automatic replacement
	my $old = $self->_error->[0];
	$self->_autoreplace->{$old} = $new;

	# do the replacement
	$self->_on_butreplace_clicked;
}

#
# $self->_on_butreplace_clicked;
#
# handler called when the replace button has been clicked.
#
sub _on_butreplace_clicked {
	my ($self) = @_;
	my $list = $self->_list;

	# get replacing word
	my $id = $list->GetNextItem( -1, Wx::wxLIST_NEXT_ALL, Wx::wxLIST_STATE_SELECTED );
	return if $id == -1;
	my $new = $list->GetItem($id)->GetText;

	# actually replace word in editor
	$self->_replace($new);

	# try to find next error
	$self->_next;
}


# -- private methods

#
# $self->_create;
#
# create the dialog itself.
#
# no params, no return values.
#
sub _create {
	my ($self) = @_;

	# create sizer that will host all controls
	my $sizer = Wx::GridBagSizer->new( 5, 5 );
	$sizer->AddGrowableCol(1);
	$sizer->AddGrowableRow(6);
	$self->_sizer($sizer);

	# create the controls
	$self->_create_labels;
	$self->_create_list;
	$self->_create_buttons;

	# wrap everything in a vbox to add some padding
	my $vbox = Wx::BoxSizer->new(Wx::wxVERTICAL);
	$vbox->Add( $sizer, 1, Wx::wxEXPAND | Wx::wxALL, 5 );
	$self->SetSizerAndFit($vbox);
	$vbox->SetSizeHints($self);

	# set focus on listbox
	$self->_list->SetFocus;
}

#
# $dialog->_create_buttons;
#
# create the buttons pane.
#
# no params. no return values.
#
sub _create_buttons {
	my ($self) = @_;

	my $ba  = Wx::Button->new( $self, -1,              _T('Add to dictionary') );
	my $br  = Wx::Button->new( $self, -1,              _T('Replace') );
	my $bra = Wx::Button->new( $self, -1,              _T('Replace all') );
	my $bi  = Wx::Button->new( $self, -1,              _T('Ignore') );
	my $bia = Wx::Button->new( $self, -1,              _T('Ignore all') );
	my $bc  = Wx::Button->new( $self, Wx::wxID_CANCEL, _T('Close') );
	Wx::Event::EVT_BUTTON( $self, $br,  \&_on_butreplace_clicked );
	Wx::Event::EVT_BUTTON( $self, $bra, \&_on_butreplace_all_clicked );
	Wx::Event::EVT_BUTTON( $self, $bi,  \&_on_butignore_clicked );
	Wx::Event::EVT_BUTTON( $self, $bia, \&_on_butignore_all_clicked );
	Wx::Event::EVT_BUTTON( $self, $bc,  \&_on_butclose_clicked );

	my $sizer = $self->_sizer;
	$sizer->Add( $ba,  Wx::GBPosition->new( 0, 2 ), Wx::GBSpan->new( 1, 1 ), Wx::wxEXPAND );
	$sizer->Add( $br,  Wx::GBPosition->new( 2, 2 ), Wx::GBSpan->new( 1, 1 ), Wx::wxEXPAND );
	$sizer->Add( $bra, Wx::GBPosition->new( 3, 2 ), Wx::GBSpan->new( 1, 1 ), Wx::wxEXPAND );
	$sizer->Add( $bi,  Wx::GBPosition->new( 4, 2 ), Wx::GBSpan->new( 1, 1 ), Wx::wxEXPAND );
	$sizer->Add( $bia, Wx::GBPosition->new( 5, 2 ), Wx::GBSpan->new( 1, 1 ), Wx::wxEXPAND );
	$sizer->Add( $bc,  Wx::GBPosition->new( 7, 2 ), Wx::GBSpan->new( 1, 1 ), Wx::wxEXPAND );

	$ba->Disable;
}

#
# $dialog->_create_labels;
#
# create the top labels.
#
# no params. no return values.
#
sub _create_labels {
	my ($self) = @_;
	my $sizer = $self->_sizer;

	# create the labels...
	my $label   = Wx::StaticText->new( $self, -1, _T('Not in dictionary:') );
	my $labword = Wx::StaticText->new( $self, -1, 'w' x 25 );
	$labword->SetBackgroundColour( Wx::Colour->new('#ffaaaa') );
	$labword->Refresh;
	$self->_label($labword);

	# ... and place them
	$sizer->Add( $label, Wx::GBPosition->new( 0, 0 ) );
	$sizer->Add( $labword, Wx::GBPosition->new( 0, 1 ), Wx::GBSpan->new( 1, 1 ), Wx::wxEXPAND );
}

#
# $dialog->_create_list;
#
# create the suggestions list.
#
# no params. no return values.
#
sub _create_list {
	my ($self) = @_;
	my $sizer = $self->_sizer;

	my $lab = Wx::StaticText->new( $self, -1, _T('Suggestions') );
	$sizer->Add( $lab, Wx::GBPosition->new( 1, 0 ), Wx::GBSpan->new( 1, 3 ), Wx::wxEXPAND );
	my $list = Wx::ListView->new(
		$self,
		-1,
		Wx::wxDefaultPosition,
		Wx::wxDefaultSize,
		Wx::wxLC_SINGLE_SEL,
	);
	Wx::Event::EVT_LIST_ITEM_ACTIVATED( $self, $list, \&_on_butreplace_clicked );
	$sizer->Add(
		$list,
		Wx::GBPosition->new( 2, 0 ),
		Wx::GBSpan->new( 5, 2 ),
		Wx::wxEXPAND
	);
	$self->_list($list);
}

#
# dialog->_next;
#
# try to find next mistake, and update dialog to show this new error. if
# no error, display a message and exit.
#
# no params. no return value.
#
sub _next {
	my ($self) = @_;
	my $autoreplace = $self->_autoreplace;

	{

		# try to find next mistake
		my ( $word, $pos ) = $self->_engine->check( $self->_text );
		$self->_error( [ $word, $pos ] );

		# no mistake means we're done
		if ( not defined $word ) {
			$self->Destroy;
			$self->GetParent->message( _T('Spell check finished.'), 'Padre' );
			return;
		}

		# check if we have hit a replace all word
		if ( exists $autoreplace->{$word} ) {
			$self->_replace( $autoreplace->{$word} );
			redo; # move on to next error
		}
	}

	# update gui with new error
	$self->_update;
}

#
# $self->_replace( $word );
#
# fix current error by replacing faulty word with $word.
#
# no param. no return value.
#
sub _replace {
	my ( $self, $new ) = @_;
	my $editor = Padre::Current->editor;

	# replace word in editor
	my $error  = $self->_error;
	my $offset = $self->_offset;
	my ( $word, $pos ) = @$error;
	my $from = $offset + $pos + $self->_engine->_utf_chars;
	my $to   = $from + length Encode::encode_utf8($word);
	$editor->SetSelection( $from, $to );
	$editor->ReplaceSelection($new);

	# FIXME: as soon as STC issue is resolved:
	# Include UTF8 characters from newly added word
	# to overall count of UTF8 characters
	# so we can set proper selections
	$self->_engine->_count_utf_chars($new);

	# remove the beginning of the text, up to after replaced word
	my $posold = $pos + length $word;
	my $posnew = $pos + length $new;
	my $text   = substr $self->_text, $posold;
	$self->_text($text);
	$offset += $posnew;
	$self->_offset($offset);
}

#
# self->_update;
#
# update the dialog box with current error.
#
sub _update {
	my ($self) = @_;
	my $error = $self->_error;
	my ( $word, $pos ) = @$error;

	# update selection in parent window
	my $editor = Padre::Current->editor;
	my $offset = $self->_offset;
	my $from   = $offset + $pos + $self->_engine->_utf_chars;
	my $to     = $from + length Encode::encode_utf8($word);
	$editor->goto_pos_centerize($from);
	$editor->SetSelection( $from, $to );

	# update label
	$self->_label->SetLabel($word);

	# update list
	my @suggestions = $self->_engine->suggestions($word);
	my $list        = $self->_list;
	$list->DeleteAllItems;
	my $i = 0;
	foreach my $w ( reverse @suggestions ) {
		next unless defined $w;
		my $item = Wx::ListItem->new;
		$item->SetText($w);
		my $idx = $list->InsertItem($item);
		last if ++$i == 25; # FIXME: should be a preference
	}

	# select first item
	my $item = $list->GetItem(0);
	$item->SetState(Wx::wxLIST_STATE_SELECTED);
	$list->SetItem($item);
}


1;



=pod

=head1 NAME

Padre::Plugin::SpellCheck::Dialog - Spell check dialog for Padre

=head1 VERSION

version 1.21

=head1 DESCRIPTION

This module implements the dialog window that will be used to interact
with the user when mistakes have been spotted.

=head1 PUBLIC METHODS

=head2 Constructor

=over 4

=item my $dialog = PPS::Dialog->new( %params );

Create and return a new dialog window. The following params are needed:

=over 4

=item text => $text

The text being spell checked.

=item offset => $offset

The offset of C<$text> within the editor. 0 if spell checking the whole file.

=item error => [ $word, $pos ]

The first spotted error, on C<$word> (at position C<$pos>), with some
associated C<$suggestions> (a list reference).

=item engine => $engine

The $engine being used (a C<Padre::Plugin::SpellCheck::Engine> object).

=back

=back

=head2 Instance methods

=over 4

=back

=head1 SEE ALSO

For all related information (bug reporting, source code repository,
etc.), refer to L<Padre::Plugin::SpellCheck>.

=head1 AUTHORS

=over 4

=item *

Fayland Lam <fayland at gmail.com>

=item *

Jerome Quelin <jquelin@gmail.com>

=item *

Ahmad M. Zawawi <ahmad.zawawi@gmail.com>

=back

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2010 by Fayland Lam, Jerome Quelin.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut


__END__