package Kephra::Edit;
our $VERSION = '0.42';
use strict;
use warnings;
#
# internal helper function
#
sub _ep_ref { Kephra::App::EditPanel::_ref() }
sub _keep_focus{ Wx::Window::SetFocus( _ep_ref() ) }
sub _let_caret_visible {
my $ep = _ep_ref();
my ($selstart, $selend) = $ep->GetSelection;
my $los = $ep->LinesOnScreen;
if ( $selstart == $selend ) {
$ep->ScrollToLine($ep->GetCurrentLine - ( $los / 2 ))
unless $ep->GetLineVisible( $ep->GetCurrentLine() );
} else {
my $startline = $ep->LineFromPosition($selstart);
my $endline = $ep->LineFromPosition($selend);
$ep->ScrollToLine( $startline - (($los - $endline - $startline) / 2) )
unless $ep->GetLineVisible($startline)
and $ep->GetLineVisible($endline);
}
$ep->EnsureCaretVisible;
}
sub _center_caret {
my $ep = _ep_ref();
my $line = $ep->GetCurrentLine();
$ep->ScrollToLine( $line - ( $ep->LinesOnScreen / 2 ));
$ep->EnsureVisible($line);
$ep->EnsureCaretVisible();
}
my @pos_stack;
sub _save_positions {
my $ep = _ep_ref();
my %pos;
$pos{document} = Kephra::Document::Data::current_nr();
$pos{pos} = $ep->GetCurrentPos;
$pos{line} = $ep->GetCurrentLine;
$pos{col} = $ep->GetColumn( $pos{pos} );
$pos{sel_begin} = $ep->GetSelectionStart;
$pos{sel_end} = $ep->GetSelectionEnd;
push @pos_stack, \%pos;
}
sub _restore_positions {
my $ep = _ep_ref();
my %pos = %{ pop @pos_stack };
if (%pos) {
Kephra::Document::Change::to_number( $pos{document} )
if $pos{document} != Kephra::Document::Data::current_nr();
$ep->SetCurrentPos( $pos{pos} );
$ep->GotoLine( $pos{line} ) if $ep->GetCurrentLine != $pos{line};
if ( $ep->GetColumn( $ep->GetCurrentPos ) == $pos{col} ) {
$ep->SetSelection( $pos{sel_begin}, $pos{sel_end} );
} else {
my $npos = $ep->PositionFromLine( $pos{line} ) + $pos{col};
my $max = $ep->GetLineEndPosition( $pos{line} );
$npos = $max if $npos > $max;
$ep->SetCurrentPos($npos);
$ep->SetSelection( $npos, $npos );
}
}
&_let_caret_visible;
}
sub _select_all_if_none {
my $ep = _ep_ref();
my ($start, $end) = $ep->GetSelection;
if ( $start == $end ) {
$ep->SelectAll;
($start, $end) = $ep->GetSelection;
}
return $ep->GetTextRange( $start, $end );
}
sub _selection_left_to_right {
my $ep = shift || _ep_ref();
my ($start, $end) = $ep->GetSelection;
my $pos = $ep->GetCurrentPos;
return -1 if $start == $end;
return $start == $pos ? 0 : 1;
}
sub _nearest_grid_pos { # position in document from given line and column
my $line = shift;
my $col = shift;
my $ep = shift || _ep_ref();
return unless defined $col;
# first staight foreward attempt
my $lpos = $ep->PositionFromLine($line);
my $pos = $lpos + $col;
return $pos if $ep->GetColumn($pos) == $col
and $ep->LineFromPosition($pos) == $line;
# if line too short take last pos of line
my $endpos = $ep->GetLineEndPosition($line);
return $endpos if $ep->GetColumn($endpos) < $col;
# if tabs used calculate
my $ipos = $ep->GetLineIndentation($line);
my $icol = $ep->GetColumn($ipos);
return $ipos + $col - $icol if $icol <= $col;
# if between indenting tabs take neares
my $tabsize = $ep->GetTabWidth();
my $tabs = $col / $tabsize;
return ($col % $tabsize < $tabsize / 2) ? $lpos + $tabs : $lpos + $tabs + 1;
}
sub can_paste { _ep_ref()->CanPaste }
sub can_copy { Kephra::Document::Data::attr('text_selected') }
#
# simple textedit
#
sub cut { _ep_ref()->Cut }
sub copy {
my $ep = _ep_ref();
$ep->Copy;
$ep->SelectionIsRectangle()
? Kephra::Document::Data::set_value('copied_rect_selection',get_clipboard_text())
: Kephra::Document::Data::set_value('copied_rect_selection','');
}
sub paste {
my $lch = Kephra::Document::Data::get_value('copied_rect_selection');
my $cb = get_clipboard_text();
(defined $lch and $lch eq $cb) ? paste_rectangular($cb) : _ep_ref()->Paste;
}
sub paste_rectangular {
my $text = shift || get_clipboard_text();
my $ep = shift || _ep_ref();
my $dragpos = shift;
my $droppos = shift;
# all additional parameters have to be provided or no one
return -1 if defined $dragpos and not defined $droppos;
my @lines = split( /[\r\n]+/, $text);
$droppos = $ep->GetCurrentPos unless defined $dragpos;
my $linenr = $ep->LineFromPosition( $droppos );
my $colnr = $ep->GetColumn($droppos );
if (defined $dragpos){
# calculate real drop position if dragged foreward
# because selection is cut out before inserted and this changed droppos
if ($dragpos <= $droppos){
my $selwidth = length $lines[0];
my $dnddelta = $linenr - $ep->LineFromPosition( $dragpos );
my $max = scalar @lines;
#$dnddelta = $max < $dnddelta ? $max : $dnddelta;
#$dnddelta *= $selwidth;
#$droppos -= $dnddelta;
#print "$dragpos ---$droppos\n";
}
}
$ep->BeginUndoAction;
$ep->ReplaceSelection(''),$ep->SetCurrentPos($droppos) if defined $dragpos;
my $insertpos;
for my $line (@lines){
$insertpos = $ep->PositionFromLine($linenr) + $colnr;
$insertpos += $colnr - $ep->GetColumn( $insertpos ) ;
$insertpos = $ep->GetLineEndPosition($linenr)
if $ep->LineFromPosition( $insertpos ) > $linenr;
$ep->InsertText( $insertpos, $line);
$linenr++;
}
$ep->EndUndoAction;
}
sub replace {
my $ep = _ep_ref();
my $text = get_clipboard_text();
copy();
_ep_ref()->ReplaceSelection($text);
}
sub clear { _ep_ref()->Clear }
sub get_clipboard_text {
my $cboard = &Wx::wxTheClipboard;
my $text;
$cboard->Open;
if ( $cboard->IsSupported( &Wx::wxDF_TEXT ) ) {
my $data = Wx::TextDataObject->new;
my $ok = $cboard->GetData( $data );
if ( $ok ) {
$text = $data->GetText;
} else {
# todo: error handling
}
}
$cboard->Close;
return defined $text ? $text : -1;
}
sub del_back_tab{
my $ep = _ep_ref();
my $pos = $ep->GetCurrentPos();
my $tab_size = Kephra::Document::Data::attr('tab_size');
my $deltaspace = $ep->GetColumn($pos--) % $tab_size;
$deltaspace = $tab_size unless $deltaspace;
do { $ep->CmdKeyExecute(&Wx::wxSTC_CMD_DELETEBACK) }
while $ep->GetCharAt(--$pos) == 32 and --$deltaspace;
}
#
# Edit Selection
#
sub get_selection { _ep_ref()->GetSelectedText() }
sub move_target {
my $linedelta = shift;
return unless defined $linedelta;
my $ep = shift || _ep_ref();
my $targetstart = $ep->GetTargetStart();
my $targettext = $ep->GetTextRange($targetstart, $ep->GetTargetEnd());
$ep->BeginUndoAction;
$ep->ReplaceTarget('');
$ep->InsertText($targetstart+$linedelta, $targettext);
$ep->EndUndoAction;
}
sub move_selection {
my $linedelta = shift;
return unless defined $linedelta;
my $ep = shift || _ep_ref();
my ($selbegin, $selend) = $ep->GetSelection();
my $targettext = $ep->GetSelectedText();
$ep->BeginUndoAction;
$ep->ReplaceSelection('');
my $pos = $ep->GetCurrentPos;
$pos += $linedelta;
$ep->InsertText($pos, $targettext);
$ep->SetSelection($pos, $pos + $selend - $selbegin);
$ep->EndUndoAction;
}
sub move_lines {
my $linedelta = shift;
return unless defined $linedelta;
my $ep = shift || _ep_ref();
my ( $selbegin, $selend) = $ep->GetSelection();
my $sellength = $selend - $selbegin;
my $selstartline = $ep->LineFromPosition($selbegin);
my $targetstart = $ep->GetTargetStart();
my $targetend = $ep->GetTargetEnd();
my $blockbegin = $ep->PositionFromLine($selstartline);
my $blockend = $ep->PositionFromLine( $ep->LineFromPosition($selend)+1 );
my $selcolumn = $selbegin - $blockbegin;
# endmode is taken when last line on start or end of operation has no EOL
# then i take the the EOL char from the line before instead and have to
# insert in a pos before to keep consistent
my $endmode;
if ($blockend == $ep->GetLength()
or $ep->LineFromPosition($selend) + $linedelta >= $ep->GetLineCount()-1 ) {
$blockbegin = $ep->GetLineEndPosition($selstartline-1);
$blockend = $ep->GetLineEndPosition( $ep->LineFromPosition($selend) );
$endmode = 1;
}
$selstartline += $linedelta;
my $blocktext = $ep->GetTextRange($blockbegin, $blockend);
$ep->BeginUndoAction;
$ep->SetTargetStart( $blockbegin );
$ep->SetTargetEnd( $blockend );
$ep->ReplaceTarget('');
$selstartline = 0 if $selstartline < 0;
$selstartline = $ep->GetLineCount() if $selstartline > $ep->GetLineCount();
my $target = $endmode
? $ep->GetLineEndPosition($selstartline-1)
: $ep->PositionFromLine($selstartline);
$ep->InsertText($target, $blocktext);
$selbegin = $ep->PositionFromLine($selstartline) + $selcolumn;
$ep->SetSelection($selbegin, $selbegin + $sellength);
$ep->SetTargetStart($targetstart );
$ep->SetTargetEnd( $targetend );
$ep->EndUndoAction;
}
sub selection_move_left {
my $ep = shift || _ep_ref();
my ($selbegin, $selend) = $ep->GetSelection();
if ( $selbegin == $selend
or $ep->LineFromPosition( $selbegin ) != $ep->LineFromPosition( $selend ) ) {
Kephra::Edit::Format::dedent_tab();
}
else {
my $newpos = $ep->WordStartPosition($selbegin, 1);
my $move_delta = $newpos == $selbegin ? -1 : $newpos - $selbegin;
move_selection( $move_delta );
}
}
sub selection_move_right{
my $ep = _ep_ref();
my ($selbegin, $selend) = $ep->GetSelection();
my $endline = $ep->LineFromPosition( $selend );
if ( $selbegin == $selend or $ep->LineFromPosition( $selbegin ) != $endline ) {
Kephra::Edit::Format::indent_tab();
}
else {
my $newpos = $ep->WordEndPosition($selend, 1);
my $move_delta = $newpos == $selend ? 1 : $newpos - $selend;
move_selection( $move_delta )
unless $endline == $ep->GetLineCount() - 1
and $ep->GetLineEndPosition($endline) == $selend;
}
}
sub selection_move_up {
my $ep = shift || _ep_ref();
my ($selbegin, $selend) = $ep->GetSelection();
my $firstline = $ep->LineFromPosition( $selbegin );
my $lastline = $ep->LineFromPosition( $selend );
if ( $selbegin != $selend and $firstline == $lastline) {
my $line = $firstline;
my $col = $ep->GetColumn( $selbegin );
return unless $line;
$line--;
move_selection( _nearest_grid_pos($line, $col) - $selbegin );
}
else { move_lines( -1, $ep ) }
}
sub selection_move_down {
my $ep = shift || _ep_ref();
my ($selbegin, $selend) = $ep->GetSelection();
my $firstline = $ep->LineFromPosition( $selbegin );
my $lastline = $ep->LineFromPosition( $selend );
if ($selbegin != $selend and $firstline == $lastline) {
my $line = $firstline;
my $col = $ep->GetColumn( $selbegin );
return if $line+1 == $ep->GetLineCount();
$line++;
move_selection( _nearest_grid_pos($line, $col) - $selend );
}
else { move_lines( 1, $ep ) }
}
sub selection_move_page_up {
my $ep = shift || _ep_ref();
my ($selbegin, $selend) = $ep->GetSelection();
my $firstline = $ep->LineFromPosition( $selbegin );
my $lastline = $ep->LineFromPosition( $selend );
my $linedelta = $ep->LinesOnScreen;
if ($selbegin != $selend and $firstline == $lastline) {
my $line = $firstline;
my $col = $ep->GetColumn( $selbegin );
return unless $line;
$line -= $linedelta;
$line = 0 if $line < 0;
move_selection( _nearest_grid_pos($line, $col) - $selbegin );
}
else { move_lines( -$linedelta, $ep ) }
}
sub selection_move_page_down {
my $ep = shift || _ep_ref();
my ($selbegin, $selend) = $ep->GetSelection();
my $firstline = $ep->LineFromPosition( $selbegin );
my $lastline = $ep->LineFromPosition( $selend );
my $linedelta = $ep->LinesOnScreen;
if ($selbegin != $selend and $firstline == $lastline) {
my $line = $firstline;
my $col = $ep->GetColumn( $selbegin );
return if $line+1 == $ep->GetLineCount();
$line += $linedelta;
$line = $ep->GetLineCount()-1 if $line >= $ep->GetLineCount();
move_selection( _nearest_grid_pos($line, $col) - $selend );
}
else { move_lines( $linedelta, $ep ) }
}
#
sub insert {
my ($text, $pos) = @_;
return unless $text;
my $ep = _ep_ref();
$pos = $ep->GetCurrentPos unless defined $pos;
$ep->InsertText($pos, $text);
$pos += length $text;
$ep->SetSelection($pos, $pos);
}
sub insert_text { insert(@_) }
sub insert_at_pos { insert(@_) }
#
# Edit Line
#
sub cut_current_line { _ep_ref()->CmdKeyExecute(&Wx::wxSTC_CMD_LINECUT) }
sub copy_current_line{ _ep_ref()->CmdKeyExecute(&Wx::wxSTC_CMD_LINECOPY)}
sub double_current_line {
my $ep = _ep_ref();
my $pos = $ep->GetCurrentPos;
$ep->BeginUndoAction;
$ep->CmdKeyExecute(&Wx::wxSTC_CMD_LINECOPY);
$ep->CmdKeyExecute(&Wx::wxSTC_CMD_PASTE);
$ep->GotoPos($pos);
$ep->EndUndoAction;
}
sub replace_current_line {
my $ep = _ep_ref();
my $line = $ep->GetCurrentLine;
$ep->BeginUndoAction;
$ep->GotoLine($line);
$ep->Paste;
$ep->SetSelection(
$ep->GetSelectionEnd,
$ep->GetLineEndPosition( $ep->GetCurrentLine )
);
$ep->Cut;
$ep->GotoLine($line);
$ep->EndUndoAction;
}
sub del_current_line{_ep_ref()->CmdKeyExecute(&Wx::wxSTC_CMD_LINEDELETE)}
sub del_line_left {_ep_ref()->DelLineLeft() }
sub del_line_right {_ep_ref()->DelLineRight()}
sub eval_newline_sub{}
1;
__END__
=head1 NAME
Kephra::Edit - basic edit menu calls and internals for editing
=head1 DESCRIPTION
=cut