# Copyright (c) 1999 Greg Bartels. All rights reserved. # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. # Special thanks to Nick Ing-Simmons for pushing a lot of # my text edit functionality into Text.pm and TextUndo.pm # otherwise, this module would have been monstrous. # Andy Worhal had it wrong, its "fifteen megabytes of fame" # -Greg Bartels package Tk::TextEdit; use vars qw($VERSION); $VERSION = '4.004'; # $Id: //depot/Tkutf8/Tk/TextEdit.pm#4 $ use Tk qw (Ev); use AutoLoader; use Text::Tabs; use base qw(Tk::TextUndo); Construct Tk::Widget 'TextEdit'; ####################################################################### ####################################################################### sub ClassInit { my ($class,$mw) = @_; $class->SUPER::ClassInit($mw); $mw->bind($class,'', 'IndentSelectedLines'); $mw->bind($class,'', 'UnindentSelectedLines'); $mw->bind($class,'', 'CommentSelectedLines'); $mw->bind($class,'', 'UncommentSelectedLines'); return $class; } # 8 horizontal pixels in the "space" character in default font. my $tab_multiplier = 8; sub debug_code_f1 { my $w=shift; } sub debug_code_f2 { my $w=shift; } ####################################################################### ####################################################################### sub InitObject { my ($w) = @_; $w->SUPER::InitObject; $w->{'INDENT_STRING'} = "\t"; # Greg mode=>"\t", Nick mode=>" " $w->{'LINE_COMMENT_STRING'} = "#"; # assuming perl comments my %pair_descriptor_hash = ( 'PARENS' => [ 'multiline', '(', ')', "[()]" ], 'CURLIES' => [ 'multiline', '{', '}', "[{}]" ], 'BRACES' => [ 'multiline', '[', ']', "[][]" ], 'DOUBLEQUOTE' => [ 'singleline', "\"","\"" ], 'SINGLEQUOTE' => [ 'singleline', "'","'" ], ); $w->{'HIGHLIGHT_PAIR_DESCRIPTOR_HASH_REF'}=\%pair_descriptor_hash; $w->tagConfigure ('CURSOR_HIGHLIGHT_PARENS', -foreground=>'white', -background=>'violet'); $w->tagConfigure ('CURSOR_HIGHLIGHT_CURLIES', -foreground=>'white', -background=>'blue'); $w->tagConfigure ('CURSOR_HIGHLIGHT_BRACES', -foreground=>'white', -background=>'purple'); $w->tagConfigure ('CURSOR_HIGHLIGHT_DOUBLEQUOTE', -foreground=>'black', -background=>'green'); $w->tagConfigure ('CURSOR_HIGHLIGHT_SINGLEQUOTE', -foreground=>'black', -background=>'grey'); $w->tagConfigure('BLOCK_HIGHLIGHT_PARENS', -background=>'red'); $w->tagConfigure('BLOCK_HIGHLIGHT_CURLIES', -background=>'orange'); $w->tagConfigure('BLOCK_HIGHLIGHT_BRACES', -background=>'red'); $w->tagConfigure('BLOCK_HIGHLIGHT_DOUBLEQUOTE', -background=>'red'); $w->tagConfigure('BLOCK_HIGHLIGHT_SINGLEQUOTE', -background=>'red'); $w->tagRaise('BLOCK_HIGHLIGHT_PARENS','CURSOR_HIGHLIGHT_PARENS'); $w->tagRaise('BLOCK_HIGHLIGHT_CURLIES','CURSOR_HIGHLIGHT_CURLIES'); $w->tagRaise('BLOCK_HIGHLIGHT_BRACES','CURSOR_HIGHLIGHT_BRACES'); $w->tagRaise('BLOCK_HIGHLIGHT_DOUBLEQUOTE','CURSOR_HIGHLIGHT_DOUBLEQUOTE'); $w->tagRaise('BLOCK_HIGHLIGHT_SINGLEQUOTE','CURSOR_HIGHLIGHT_SINGLEQUOTE'); $w->{'UPDATE_WIDGET_PERIOD'}=300; # how much time between each call. $w->{'WINDOW_PLUS_AND_MINUS_VALUE'}=80; $w->SetGUICallbackIndex(0); $w->schedule_next_callback; } ####################################################################### sub cancel_current_gui_callback_and_restart_from_beginning { my ($w)=@_; if(defined($w->{'UPDATE_WIDGET_AFTER_REFERENCE'})) {$w->{'UPDATE_WIDGET_AFTER_REFERENCE'}->cancel();} $w->SetGUICallbackIndex(0); $w->schedule_next_callback; } sub schedule_next_callback { my ($w)=@_; return if $w->NoMoreGUICallbacksToCall; #stops infinite recursive call. $w->{'UPDATE_WIDGET_AFTER_REFERENCE'} = $w->after ($w->{'UPDATE_WIDGET_PERIOD'}, sub { $w->CallNextGUICallback; $w->schedule_next_callback; } ); } ####################################################################### # use these methods to pass the TextEdit widget an anonymous array # of code references. # any time the widget changes that requires the display to be updated, # then these code references will be scheduled in sequence for calling. # splitting them up allows them to be prioritized by order, # and prevents the widget from "freezing" too long if they were # one large callback. scheduling them apart allows the widget time # to respond to user inputs. ####################################################################### sub SetGUICallbacks { my ($w,$callback_array_ref) = @_; $w->{GUI_CALLBACK_ARRAY_REF}=$callback_array_ref; $w->SetGUICallbackIndex(0); } sub GetGUICallbacks { return shift->{GUI_CALLBACK_ARRAY_REF}; } sub SetGUICallbackIndex { my ($w, $val)=@_; $w->{GUI_CALLBACK_ARRAY_INDEX}=$val; } sub GetGUICallbackIndex { return shift->{GUI_CALLBACK_ARRAY_INDEX}; } sub IncrementGUICallbackIndex { shift->{GUI_CALLBACK_ARRAY_INDEX} += 1; } sub NoMoreGUICallbacksToCall { my ($w) = @_; return 0 unless defined ($w->{GUI_CALLBACK_ARRAY_REF}); return 0 unless defined ($w->{GUI_CALLBACK_ARRAY_INDEX}); my $arr_ref = $w->{GUI_CALLBACK_ARRAY_REF}; my $arr_ind = $w->{GUI_CALLBACK_ARRAY_INDEX}; return $arr_ind >= @$arr_ref; } sub CallNextGUICallback { my ($w) = @_; return if $w->NoMoreGUICallbacksToCall; my $arr_ref = $w->{GUI_CALLBACK_ARRAY_REF}; my $arr_ind = $w->{GUI_CALLBACK_ARRAY_INDEX}; &{$arr_ref->[$arr_ind]}; $w->IncrementGUICallbackIndex; } ####################################################################### ####################################################################### sub insert { my $w = shift; $w->SUPER::insert(@_); $w->cancel_current_gui_callback_and_restart_from_beginning; } sub delete { my $w = shift; $w->SUPER::delete(@_); $w->cancel_current_gui_callback_and_restart_from_beginning; } sub SetCursor { my $w = shift; $w->SUPER::SetCursor(@_); $w->cancel_current_gui_callback_and_restart_from_beginning; } sub OverstrikeMode { my ($w,$mode) = @_; if (defined($mode)) { $w->SUPER::OverstrikeMode($mode); $w->cancel_current_gui_callback_and_restart_from_beginning; } return $w->SUPER::OverstrikeMode; } ####################################################################### # use yview on scrollbar to get fractional coordinates. # scale this by the total length of the text to find the # approximate start line of widget and end line of widget. ####################################################################### sub GetScreenWindowCoordinates { my $w = shift; my ($top_frac, $bot_frac) = $w->yview; my $end_index = $w->index('end'); my ($lines,$columns) = split (/\./,$end_index); my $window = $w->{'WINDOW_PLUS_AND_MINUS_VALUE'}; my $top_line = int(($top_frac * $lines) - $window); $top_line = 0 if ($top_line < 0); my $bot_line = int(($bot_frac * $lines) + $window); $bot_line = $lines if ($bot_line > $lines); my $top_index = $top_line . '.0'; my $bot_index = $bot_line . '.0'; $_[0] = $top_index; $_[1] = $bot_index; } ######################################################################## # take two indices as inputs. # if they are on the same line or same column (accounting for tabs) # then return 1 # else return 0 # (assume indices passed in are in line.column format) ######################################################################## sub IndicesLookGood { my ($w, $start, $end, $singleline) = @_; return 0 unless ( (defined($start)) and (defined($end))); my ($start_line, $start_column) = split (/\./,$start); my ($end_line, $end_column) = split (/\./,$end); ########################## # good if on the same line ########################## return 1 if ($start_line == $end_line); ########################## # if not on same line and its a singleline, its bad ########################## return 0 if $singleline; # get both lines, convert the tabs to spaces, and get the new column. # see if they line up or not. my $string; $string = $w->get($start_line.'.0', $start_line.'.0 lineend'); $string = substr($string, 0, $start_column+1); $string = expand($string); $start_column = length($string); $string = $w->get($end_line.'.0', $end_line.'.0 lineend'); $string = substr($string, 0, $end_column +1); $string = expand($string); $end_column = length($string); ########################## # good if on the same column (adjusting for tabs) ########################## return 1 if ($start_column == $end_column); # otherwise its bad return 0; } ######################################################################## # if searching backward, count paranthesis until find a start parenthesis # which does not have a forward match. # # (<= search backward will return this index # () # START X HERE # ( ( ) () ) # )<== search forward will return this index # # if searching forward, count paranthesis until find a end parenthesis # which does not have a rearward match. ######################################################################## sub searchForBaseCharacterInPair { my ( $w, $top_index, $searchfromindex, $bot_index, $direction, $startchar, $endchar, $charpair )=@_; my ($plus_one_char, $search_end_index, $index_offset, $done_index); if ($direction eq '-forward') { $plus_one_char = $endchar; $search_end_index = $bot_index; $index_offset = ' +1c'; $done_index = $w->index('end'); } else { $plus_one_char = $startchar; $search_end_index = $top_index; $index_offset = ''; $done_index = '1.0'; } my $at_done_index = 0; my $count = 0; my $char; while(1) { $searchfromindex = $w->search ($direction, '-regexp', $charpair, $searchfromindex, $search_end_index ); last unless(defined($searchfromindex)); $char = $w->get($searchfromindex, $w->index($searchfromindex.' +1c')); if ($char eq $plus_one_char) {$count += 1;} else {$count -= 1;} last if ($count==1); # boundary condition exists when first char in widget is the match char # need to be able to determine if search tried to go past index '1.0' # if so, set index to undef and return. if ( $at_done_index ) { $searchfromindex = undef; last; } $at_done_index = 1 if ($searchfromindex eq $done_index); $searchfromindex=$w->index($searchfromindex . $index_offset); } return $searchfromindex; } ######################################################################## # highlight a character pair that most closely brackets the cursor. # allows you to pick and choose which ones you want to do. ######################################################################## sub HighlightParenthesisAroundCursor { my ($w)=@_; $w->HighlightSinglePairBracketingCursor ( '(', ')', '[()]', 'CURSOR_HIGHLIGHT_PARENS','BLOCK_HIGHLIGHT_PARENS',0); } sub HighlightCurlyBracesAroundCursor { my ($w)=@_; $w->HighlightSinglePairBracketingCursor ( '{', '}', '[{}]', 'CURSOR_HIGHLIGHT_CURLIES','BLOCK_HIGHLIGHT_CURLIES',0); } sub HighlightBracesAroundCursor { my ($w)=@_; $w->HighlightSinglePairBracketingCursor ( '[', ']','[][]', 'CURSOR_HIGHLIGHT_BRACES','BLOCK_HIGHLIGHT_BRACES',0); } sub HighlightDoubleQuotesAroundCursor { my ($w)=@_; $w->HighlightSinglePairBracketingCursor ( "\"", "\"", "\"", 'CURSOR_HIGHLIGHT_DOUBLEQUOTE','BLOCK_HIGHLIGHT_DOUBLEQUOTE',1); } sub HighlightSingleQuotesAroundCursor { my ($w)=@_; $w->HighlightSinglePairBracketingCursor ( "'", "'", "'", 'CURSOR_HIGHLIGHT_SINGLEQUOTE','BLOCK_HIGHLIGHT_SINGLEQUOTE',1); } ######################################################################## # highlight all the character pairs that most closely bracket the cursor. ######################################################################## sub HighlightAllPairsBracketingCursor { my ($w)=@_; $w->HighlightParenthesisAroundCursor; $w->HighlightCurlyBracesAroundCursor; $w->HighlightBracesAroundCursor; $w->HighlightDoubleQuotesAroundCursor; $w->HighlightSingleQuotesAroundCursor; } ######################################################################## # search for a pair of matching characters that bracket the # cursor and tag them with the given tagname. # startchar might be '[' # endchar would then be ']' # tagname is a name of a tag, which has already been # configured to highlight however the user wants them to behave. # error tagname is the tag to highlight the chars with if there # is a problem of some kind. # singleline indicates whether the character pairs must occur # on a single line. quotation marks are single line characters usually. ######################################################################## sub HighlightSinglePairBracketingCursor { my ( $w, $startchar, $endchar, $charpair, $good_tagname, $bad_tagname, $single_line ) = @_; $single_line=0 unless defined($single_line); $w->tagRemove($good_tagname, '1.0','end'); $w->tagRemove($bad_tagname, '1.0','end'); my $top_index; my $bot_index; my $cursor = $w->index('insert'); if ($single_line) { $top_index = $w->index($cursor.' linestart'); $bot_index = $w->index($cursor.' lineend'); } else { $w->GetScreenWindowCoordinates($top_index, $bot_index); } # search backward for the startchar # $top_index, $searchfromindex, $bot_index, # $direction, $startchar, $endchar, $charpair my $startindex = $w->searchForBaseCharacterInPair ( $top_index, $cursor, $bot_index, '-backward', $startchar, $endchar, $charpair ); # search forward for the endchar my $endindex = $w->searchForBaseCharacterInPair ( $top_index, $cursor, $bot_index, '-forward', $startchar, $endchar, $charpair ); return unless ((defined $startindex) and (defined $endindex)); my $final_tag = $bad_tagname; if ($w->IndicesLookGood( $startindex, $endindex, $single_line)) { $final_tag = $good_tagname; } $w->tagAdd($final_tag, $startindex, $w->index($startindex.'+1c') ); $w->tagAdd($final_tag, $endindex, $w->index( $endindex.'+1c') ); } #################################################################### sub IndentSelectedLines { my($w)=@_; $w->insertStringAtStartOfSelectedLines($w->{'INDENT_STRING'}); } sub UnindentSelectedLines { my($w)=@_; $w->deleteStringAtStartOfSelectedLines($w->{'INDENT_STRING'}); } sub CommentSelectedLines { my($w)=@_; $w->insertStringAtStartOfSelectedLines($w->{'LINE_COMMENT_STRING'}); } sub UncommentSelectedLines { my($w)=@_; $w->deleteStringAtStartOfSelectedLines($w->{'LINE_COMMENT_STRING'}); } 1; __END__