The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Padre::Wx::Dialog::OpenResource;

use 5.008;
use strict;
use warnings;
use Cwd                   ();
use Padre::DB             ();
use Padre::MIME           ();
use Padre::Role::Task     ();
use Padre::Wx             ();
use Padre::Wx::Icon       ();
use Padre::Wx::Role::Idle ();
use Padre::Wx::Role::Main ();
use Padre::Logger;

our $VERSION = '1.00';
our @ISA     = qw{
	Padre::Role::Task
	Padre::Wx::Role::Idle
	Padre::Wx::Role::Main
	Wx::Dialog
};

# -- constructor
sub new {
	my $class = shift;
	my $main  = shift;

	# Create object
	my $self = $class->SUPER::new(
		$main,
		-1,
		Wx::gettext('Open Resources'),
		Wx::DefaultPosition,
		Wx::DefaultSize,
		Wx::DEFAULT_FRAME_STYLE | Wx::TAB_TRAVERSAL,
	);

	$self->init_search;

	# Dialog's icon as is the same as Padre
	$self->SetIcon(Padre::Wx::Icon::PADRE);

	# Create dialog
	$self->_create;

	return $self;
}


#
# Initialize search
#
sub init_search {
	my $self     = shift;
	my $current  = $self->current;
	my $document = $current->document;
	my $filename = $current->filename;
	my $project  = $current->project;

	# Check if we have an open file so we can use its directory
	my $directory = $filename

		# Current document's project or base directory
		? $project
			? $project->root
			: File::Basename::dirname($filename)

			# Current working directory
		: Cwd::getcwd();

	# Restart search if the project/current directory is different
	my $previous = $self->{directory};
	if ( $previous && $previous ne $directory ) {
		$self->{matched_files} = undef;
	}

	$self->{directory} = $directory;
	$self->SetLabel( Wx::gettext('Open Resources') . ' - ' . $directory );
}

# -- event handler

#
# handler called when the ok button has been clicked.
#
sub ok_button {
	my $self = shift;
	my $main = $self->main;
	$self->Hide;

	# Open the selected resources here if the user pressed OK
	my $lock = $main->lock('DB');
	my $list = $self->{matches_list};
	foreach my $selection ( $list->GetSelections ) {
		my $filename = $list->GetClientData($selection);

		# Fetch the recently used files from the database
		require Padre::DB::RecentlyUsed;
		my $recent = Padre::DB::RecentlyUsed->select(
			"where type = ? and value = ?",
			'RESOURCE',
			$filename,
		) || [];

		my $found = scalar @$recent > 0;

		eval {

			# Try to open the file now
			if ( my $id = $main->editor_of_file($filename) ) {
				my $page = $main->notebook->GetPage($id);
				$page->SetFocus;
			} else {
				$main->setup_editors($filename);
			}
		};
		if ($@) {
			$main->error( sprintf( Wx::gettext('Error while trying to perform Padre action: %s'), $@ ) );
			TRACE("Error while trying to perform Padre action: $@") if DEBUG;
		} else {

			# And insert a recently used tuple if it is not found
			# and the action is successful.
			if ($found) {
				Padre::DB->do(
					"update recently_used set last_used = ? where name = ? and type = ?",
					{}, time(), $filename, 'RESOURCE',
				);
			} else {
				Padre::DB::RecentlyUsed->create(
					name      => $filename,
					value     => $filename,
					type      => 'RESOURCE',
					last_used => time(),
				);
			}
		}
	}

}


# -- private methods

#
# create the dialog itself.
#
sub _create {
	my $self = shift;

	# create sizer that will host all controls
	$self->{sizer} = Wx::BoxSizer->new(Wx::VERTICAL);

	# create the controls
	$self->_create_controls;
	$self->_create_buttons;

	# wrap everything in a vbox to add some padding
	$self->SetMinSize( [ 360, 340 ] );
	$self->SetSizer( $self->{sizer} );

	# center/fit the dialog
	$self->Fit;
	$self->CentreOnParent;
}

#
# create the buttons pane.
#
sub _create_buttons {
	my $self = shift;

	$self->{ok_button} = Wx::Button->new(
		$self,
		Wx::ID_OK,
		Wx::gettext('&OK'),
	);
	$self->{ok_button}->SetDefault;
	$self->{cancel_button} = Wx::Button->new(
		$self,
		Wx::ID_CANCEL,
		Wx::gettext('&Cancel'),
	);

	my $buttons = Wx::BoxSizer->new(Wx::HORIZONTAL);
	$buttons->AddStretchSpacer;
	$buttons->Add( $self->{ok_button},     0, Wx::ALL | Wx::EXPAND, 5 );
	$buttons->Add( $self->{cancel_button}, 0, Wx::ALL | Wx::EXPAND, 5 );
	$self->{sizer}->Add( $buttons, 0, Wx::ALL | Wx::EXPAND | Wx::ALIGN_CENTER, 5 );

	Wx::Event::EVT_BUTTON( $self, Wx::ID_OK, \&ok_button );
}

#
# create controls in the dialog
#
sub _create_controls {
	my $self = shift;

	# search textbox
	my $search_label = Wx::StaticText->new(
		$self,
		-1,
		Wx::gettext('&Select an item to open (? = any character, * = any string):')
	);
	$self->{search_text} = Wx::TextCtrl->new(
		$self,
		-1,
		'',
		Wx::DefaultPosition,
		Wx::DefaultSize,
	);
	$self->{search_text}->SetToolTip( Wx::gettext('Enter parts of the resource name to find it') );

	$self->{popup_button} = Wx::BitmapButton->new(
		$self,
		-1,
		Padre::Wx::Icon::find("actions/go-down")
	);
	$self->{popup_button}->SetToolTip( Wx::gettext('Click on the arrow for filter settings') );

	# matches result list
	my $matches_label = Wx::StaticText->new(
		$self,
		-1,
		Wx::gettext('&Matching Items:')
	);

	$self->{matches_list} = Wx::ListBox->new(
		$self,
		-1,
		Wx::DefaultPosition,
		Wx::DefaultSize,
		[],
		Wx::LB_EXTENDED,
	);
	$self->{matches_list}->SetToolTip( Wx::gettext('Select one or more resources to open') );

	# Shows how many items are selected and information about what is selected
	$self->{status_text} = Wx::TextCtrl->new(
		$self,
		-1,
		Wx::gettext('Current Directory: ') . $self->{directory},
		Wx::DefaultPosition,
		Wx::DefaultSize,
		Wx::TE_READONLY,
	);

	my $folder_image = Wx::StaticBitmap->new(
		$self,
		-1,
		Padre::Wx::Icon::find("places/stock_folder")
	);

	$self->{copy_button} = Wx::BitmapButton->new(
		$self,
		-1,
		Padre::Wx::Icon::find("actions/edit-copy"),
	);
	$self->{copy_button}->SetToolTip( Wx::gettext('Copy filename to clipboard') );

	$self->{popup_menu}     = Wx::Menu->new;
	$self->{skip_vcs_files} = $self->{popup_menu}->AppendCheckItem(
		-1,
		Wx::gettext("Skip version control system files"),
	);
	$self->{skip_using_manifest_skip} = $self->{popup_menu}->AppendCheckItem(
		-1,
		Wx::gettext("Skip using MANIFEST.SKIP"),
	);

	$self->{skip_vcs_files}->Check(1);
	$self->{skip_using_manifest_skip}->Check(1);

	my $hb;
	$self->{sizer}->AddSpacer(10);
	$self->{sizer}->Add( $search_label, 0, Wx::ALL | Wx::EXPAND, 2 );
	$hb = Wx::BoxSizer->new(Wx::HORIZONTAL);
	$hb->AddSpacer(2);
	$hb->Add( $self->{search_text},  1, Wx::ALIGN_CENTER_VERTICAL, 2 );
	$hb->Add( $self->{popup_button}, 0, Wx::ALL | Wx::EXPAND,      2 );
	$hb->AddSpacer(1);
	$self->{sizer}->Add( $hb,                   0, Wx::BOTTOM | Wx::EXPAND, 5 );
	$self->{sizer}->Add( $matches_label,        0, Wx::ALL | Wx::EXPAND,    2 );
	$self->{sizer}->Add( $self->{matches_list}, 1, Wx::ALL | Wx::EXPAND,    2 );
	$hb = Wx::BoxSizer->new(Wx::HORIZONTAL);
	$hb->AddSpacer(2);
	$hb->Add( $folder_image,        0, Wx::ALL | Wx::EXPAND,      1 );
	$hb->Add( $self->{status_text}, 1, Wx::ALIGN_CENTER_VERTICAL, 1 );
	$hb->Add( $self->{copy_button}, 0, Wx::ALL | Wx::EXPAND,      1 );
	$hb->AddSpacer(1);
	$self->{sizer}->Add( $hb, 0, Wx::BOTTOM | Wx::EXPAND, 5 );
	$self->_setup_events;

	return;
}

#
# Adds various events
#
sub _setup_events {
	my $self = shift;

	Wx::Event::EVT_CHAR(
		$self->{search_text},
		sub {
			my $this  = shift;
			my $event = shift;
			my $code  = $event->GetKeyCode;

			$self->{matches_list}->SetFocus
				if ( $code == Wx::K_DOWN )
				or ( $code == Wx::K_UP )
				or ( $code == Wx::K_NUMPAD_PAGEDOWN )
				or ( $code == Wx::K_PAGEDOWN )
				or ( $code == Wx::K_NUMPAD_PAGEUP )
				or ( $code == Wx::K_PAGEUP );


			$event->Skip(1);
		}
	);

	Wx::Event::EVT_CHAR(
		$self->{matches_list},
		sub {
			my $this  = shift;
			my $event = shift;
			my $code  = $event->GetKeyCode;

			$self->{search_text}->SetFocus
				unless ( $code == Wx::K_DOWN )
				or ( $code == Wx::K_UP )
				or ( $code == Wx::K_NUMPAD_PAGEDOWN )
				or ( $code == Wx::K_PAGEDOWN )
				or ( $code == Wx::K_NUMPAD_PAGEUP )
				or ( $code == Wx::K_PAGEUP );

			$event->Skip(1);
		}
	);

	Wx::Event::EVT_TEXT(
		$self,
		$self->{search_text},
		sub {
			unless ( $self->{matched_files} ) {
				$self->search;
			}
			$self->render;
			return;
		}
	);

	Wx::Event::EVT_LISTBOX(
		$self,
		$self->{matches_list},
		sub {
			my $self         = shift;
			my @matches      = $self->{matches_list}->GetSelections;
			my $num_selected = scalar @matches;
			if ( $num_selected == 1 ) {
				$self->{status_text}
					->ChangeValue( $self->_path( $self->{matches_list}->GetClientData( $matches[0] ) ) );
				$self->{copy_button}->Enable(1);
			} elsif ( $num_selected > 1 ) {
				$self->{status_text}->ChangeValue( $num_selected . " items selected" );
				$self->{copy_button}->Enable(0);
			} else {
				$self->{status_text}->ChangeValue('');
				$self->{copy_button}->Enable(0);
			}

			return;
		}
	);

	Wx::Event::EVT_LISTBOX_DCLICK(
		$self,
		$self->{matches_list},
		sub {
			$self->ok_button;
		}
	);

	Wx::Event::EVT_BUTTON(
		$self,
		$self->{copy_button},
		sub {
			my @matches      = $self->{matches_list}->GetSelections;
			my $num_selected = scalar @matches;
			if ( $num_selected == 1 ) {
				if ( Wx::TheClipboard->Open ) {
					Wx::TheClipboard->SetData(
						Wx::TextDataObject->new( $self->{matches_list}->GetClientData( $matches[0] ) ) );
					Wx::TheClipboard->Close;
				}
			}
		}
	);

	Wx::Event::EVT_MENU(
		$self,
		$self->{skip_vcs_files},
		sub {
			$self->restart;
		},
	);
	Wx::Event::EVT_MENU(
		$self,
		$self->{skip_using_manifest_skip},
		sub {
			$self->restart;
		},
	);

	Wx::Event::EVT_BUTTON(
		$self,
		$self->{popup_button},
		sub {
			my ( $self, $event ) = @_;
			$self->PopupMenu(
				$self->{popup_menu},
				$self->{popup_button}->GetPosition->x,
				$self->{popup_button}->GetPosition->y + $self->{popup_button}->GetSize->GetHeight
			);
		}
	);

	$self->idle_method('show_recent');
}

#
# Restarts search
#
sub restart {
	my $self = shift;
	$self->search;
	$self->render;
}

#
# Focus on it if it shown or restart its state and show it if it is hidden.
#
sub show {
	my $self = shift;

	$self->init_search;

	if ( $self->IsShown ) {
		$self->SetFocus;
	} else {
		my $editor = $self->current->editor;
		if ($editor) {
			my $selection        = $editor->GetSelectedText;
			my $selection_length = length $selection;
			if ( $selection_length > 0 ) {
				$self->{search_text}->ChangeValue($selection);
				$self->restart;
			} else {
				$self->{search_text}->ChangeValue('');
			}
		} else {
			$self->{search_text}->ChangeValue('');
		}

		$self->idle_method('show_recent');

		$self->Show(1);
	}
}

# Show recently opened resources
sub show_recent {
	my $self = shift;
	$self->_show_recently_opened_resources;
	$self->{search_text}->SetFocus;
	return;
}

#
# Shows the recently opened resources
#
sub _show_recently_opened_resources {
	my $self = shift;

	# Fetch them from Padre's RecentlyUsed database table
	require Padre::DB::RecentlyUsed;
	my $recently_used = Padre::DB::RecentlyUsed->select( 'where type = ? order by last_used desc', 'RESOURCE' ) || [];
	my @recent_files = ();
	foreach my $e (@$recently_used) {
		push @recent_files, $self->_path( $e->value );
	}

	# Show results in matching items list
	$self->{matched_files} = \@recent_files;
	$self->render;

	# No need to store them anymore
	$self->{matched_files} = undef;
}

#
# Search for files and cache result
#
sub search {
	my $self = shift;

	$self->{status_text}->ChangeValue( Wx::gettext('Reading items. Please wait...') );

	# Kick off the resource search
	$self->task_request(
		task                     => 'Padre::Task::OpenResource',
		directory                => $self->{directory},
		skip_vcs_files           => $self->{skip_vcs_files}->IsChecked,
		skip_using_manifest_skip => $self->{skip_using_manifest_skip}->IsChecked,
	);

	return;
}

sub task_finish {
	my $self    = shift;
	my $task    = shift;
	my $matched = $task->{matched} or return;
	$self->{matched_files} = $matched;
	$self->render;
	return 1;
}

#
# Update matches list box from matched files list
#
sub render {
	my $self = shift;
	return unless $self->{matched_files};

	my $search_expr = $self->{search_text}->GetValue;

	# Quote the search string to make it safer
	# and then tranform * and ? into .* and .
	$search_expr = quotemeta $search_expr;
	$search_expr =~ s/\\\*/.*?/g;
	$search_expr =~ s/\\\?/./g;

	# Save user selections for later
	my @matches = $self->{matches_list}->GetSelections;

	# prepare more general search expression
	my $is_perl_package_expr = 0;
	if ( $search_expr =~ s/\\:\\:/\//g ) { # undo quotemeta and substitute / for ::
		$is_perl_package_expr = 1;
	}
	if ( $search_expr =~ s/\\:/\//g ) {    # undo quotemeta and substitute / for :
		$is_perl_package_expr = 1;
	}

	# Populate the list box
	$self->{matches_list}->Clear;
	my $pos = 0;
	my %contains_file;

	# direct filename matches
	foreach my $file ( @{ $self->{matched_files} } ) {
		my $filename = File::Basename::fileparse($file);
		if ( $filename =~ /^$search_expr/i ) {

			# display package name if it is a Perl file
			my $pkg       = '';
			my $mime_type = Padre::MIME->detect(
				file  => $file,
				perl6 => $self->config->lang_perl6_auto_detection,
			);
			if ( $mime_type eq 'application/x-perl' or $mime_type eq 'application/x-perl6' ) {
				my $contents = Padre::Util::slurp($file);
				if ( $contents && $$contents =~ /\s*package\s+(.+);/ ) {
					$pkg = "  ($1)";
				}
			}
			$self->{matches_list}->Insert( $filename . $pkg, $pos, $file );
			$contains_file{ $filename . $pkg } = 1;
			$pos++;
		}
	}

	# path matches
	my @ignore_path_extensions = '.t';
	foreach my $file ( @{ $self->{matched_files} } ) {
		if ( $file =~ /^$self->{directory}.+$search_expr/i ) {
			my ( $filename, $path, $suffix ) = File::Basename::fileparse( $file, @ignore_path_extensions );

			my $pkg_name = '';

			if ( length $suffix > 0 ) {
				next unless $filename =~ /$search_expr/i; # ignore path for certain files
				$filename .= $suffix;                     # add suffix again
			} else {

				# display package name if it is a Perl file
				my $mime_type = Padre::MIME->detect(
					file  => $file,
					perl6 => $self->config->lang_perl6_auto_detection,
				);
				if ( $mime_type eq 'application/x-perl' or $mime_type eq 'application/x-perl6' ) {
					my $contents = Padre::Util::slurp($file);
					if ( $contents && $$contents =~ /\s*package\s+(.+);/ ) {
						$pkg_name = "  ($1)";
					}
				} else {
					next if $is_perl_package_expr; # do nothing if input contains : or ::
				}
			}

			unless ( exists $contains_file{ $filename . $pkg_name } ) {
				$self->{matches_list}->Insert( $filename . $pkg_name, $pos, $file );
				$pos++;
			}
		}
	}

	if ( $pos > 0 ) {

		# keep the old user selection if it is possible
		$self->{matches_list}->Select( scalar @matches > 0 ? $matches[0] : 0 );
		$self->{status_text}->ChangeValue( $self->_path( $self->{matches_list}->GetClientData(0) ) );
		$self->{status_text}->Enable(1);
		$self->{copy_button}->Enable(1);
		$self->{ok_button}->Enable(1);
	} else {
		$self->{status_text}->ChangeValue('');
		$self->{status_text}->Enable(0);
		$self->{copy_button}->Enable(0);
		$self->{ok_button}->Enable(0);
	}

	return;
}

#
# Cleans a path on various platforms
#
sub _path {
	my $self = shift;
	my $path = shift;
	if (Padre::Constant::WIN32) {
		$path =~ s/\//\\/g;
	}
	return $path;
}

1;

__END__

=pod

=head1 NAME

Padre::Wx::Dialog::OpenResource - Open Resources dialog

=head1 DESCRIPTION

=head2 Open Resource (Shortcut: C<Ctrl+Shift+R>)

This opens a nice dialog that allows you to find any file that exists
in the current document or working directory. You can use C<?> to replace
a single character or C<*> to replace an entire string. The matched files list
are sorted alphabetically and you can select one or more files to be opened in
Padre when you press the B<OK> button.

You can simply ignore F<CVS>, F<.svn> and F<.git> folders using a simple check-box
(enhancement over Eclipse).

=head1 AUTHOR

Ahmad M. Zawawi E<lt>ahmad.zawawi at gmail.comE<gt>

=head1 COPYRIGHT & LICENSE

Copyright 2008-2013 The Padre development team as listed in Padre.pm.

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

=cut

# Copyright 2008-2013 The Padre development team as listed in Padre.pm.
# LICENSE
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl 5 itself.