#!perl -w # # T-Pad - A Perl/Tk GUI based Perl-script editor with syntax highlighting # # Usage: see Perl documentation in pod format (perldoc) # use strict; use Tk; { ########################################################################### package TextHighlight; ########################################################################### use vars qw($VERSION %FUNC %FLOW %OPER); $VERSION = '4.04'; my @FUNC = qw/AUTOLOAD BEGIN CHECK CORE DESTROY END INIT abs accept alarm atan2 bind binmode bless caller chdir chmod chomp chop chown chr chroot close closedir cmp connect cos crypt dbmclose dbmopen defined delete die dump each endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getppid getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt glob gmtime grep hex index int ioctl join keys kill lc lcfirst length link listen localtime log lock lstat map mkdir msgctl msgget msgrcv msgsnd new oct open opendir ord pack pipe pop pos print printf prototype push quotemeta rand read readdir readline readlink readpipe recv ref rename reset reverse rewinddir rindex rmdir scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat study substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times truncate uc ucfirst umask undef unlink unpack unshift untie utime values vec wait waitpid wantarray warn write/; my @FLOW = qw/continue do else elsif for foreach goto if last local my next our no package redo require return sub unless until use while __DATA__ __END__ __FILE__ __LINE__ __PACKAGE__/; my @OPER = qw/and eq ge gt le lt m ne not or q qq qr qw qx s tr y xor x/; # Build lookup tables @FUNC{@FUNC} = (1) x @FUNC; undef @FUNC; @FLOW{@FLOW} = (1) x @FLOW; undef @FLOW; @OPER{@OPER} = (1) x @OPER; undef @OPER; use Tk qw(Ev); use AutoLoader; # Set @TextHighlight::ISA = ('Tk::TextUndo') use base qw(Tk::TextUndo); Construct Tk::Widget 'TextHighlight'; sub ClassInit { my ($class, $mw) = @_; $class->SUPER::ClassInit($mw); $mw->bind($class, '', \&main::openDialog); $mw->bind($class, '', [\&main::addPage, 'Untitled']); $mw->bind($class, '', [\&main::saveDialog, 's']); $mw->bind($class, '', \&main::runScript); $mw->bind($class, '', \&main::commandHelp); return $class; } sub InitObject { my ($w, $args) = @_; $w->SUPER::InitObject($args); $w->tagConfigure('FUNC', -foreground => '#FF0000'); $w->tagConfigure('FLOW', -foreground => '#0000FF'); $w->tagConfigure('OPER', -foreground => '#FF8200'); $w->tagConfigure('STRG', -foreground => '#848284'); $w->tagConfigure('CMNT', -foreground => '#008284'); $w->tagConfigure('MTCH', -background => '#FFFF00'); # Default: font family courier, size 10 $w->configure(-font => $w->fontCreate(qw/-family courier -size 10/)); $w->{CALLBACK} = undef; $w->{CHANGES} = 0; $w->{LINE} = 0; } sub Button1 { my $w = shift; $w->SUPER::Button1(@_); &{$w->{CALLBACK}} if ( defined $w->{CALLBACK} ); } sub see { my $w = shift; $w->SUPER::see(@_); &{$w->{CALLBACK}} if ( defined $w->{CALLBACK} ); } # Set/Get the amount of changes sub numberChangesExt { my ($w, $changes) = @_; if ( @_ > 1 ) { $w->{CHANGES} = $changes; } return $w->{CHANGES}; } # Register callback function and call it immediately sub positionChangedCallback { my ($w, $callback) = @_; &{$w->{CALLBACK} = $callback}; } sub insert { my $w = shift; my ($s_line) = split(/\./, $w->index('insert')); $w->SUPER::insert(@_); my ($e_line) = split(/\./, $w->index('insert')); highlight($w, $s_line, $e_line); &{$w->{CALLBACK}} if ( defined $w->{CALLBACK} ); } # Insert text without highlight sub insertWHL { my $w = shift; $w->SUPER::insert(@_); } # Background highlight sub backgroundHL { my ($w, $l) = @_; my ($end) = split(/\./, $w->index('end')); $w->{LINE} = $end unless ( $w->{LINE} ); # 'cut/delete' correction if needed if ( $w->{LINE} != $end ) { $l -= ($w->{LINE} - $end); if ( $l < 0 ) { $l = 0 } $w->{LINE} = $end; } highlight($w, $l, $l+50 > $end ? $end-1 : $l+50); if ( $l+50 < $end ) { $w->after(50, [\&backgroundHL, $w, $l+50+1]); } else { $w->{LINE} = 0 } } sub insertTab { my ($w) = @_; my $pos = (split(/\./, $w->index('insert')))[1]; # Insert spaces instead of tabs $w->Insert(' ' x (4-($pos%4))); $w->focus; &{$w->{CALLBACK}} if ( defined $w->{CALLBACK} ); $w->break; } sub delete { my $w = shift; $w->SUPER::delete(@_); my ($line) = split(/\./, $w->index('insert')); highlight($w, $line, $line); } sub InsertKeypress { my $w = shift; $w->SUPER::InsertKeypress(@_); # Easy things easy... if ( $_[0] =~ /[([{<"']/ ) { $w->SUPER::InsertKeypress(')') if ( $_[0] eq '(' ); $w->SUPER::InsertKeypress(']') if ( $_[0] eq '[' ); $w->SUPER::InsertKeypress('}') if ( $_[0] eq '{' ); $w->SUPER::InsertKeypress('>') if ( $_[0] eq '<' ); $w->SUPER::InsertKeypress('"') if ( $_[0] eq '"' ); $w->SUPER::InsertKeypress("'") if ( $_[0] eq "'" ); $w->SetCursor('insert-1c'); } my ($line) = split(/\./, $w->index('insert')); highlight($w, $line, $line); &{$w->{CALLBACK}} if ( defined $w->{CALLBACK} ); } sub highlight { my ($w, $s_line, $e_line) = @_; # Remove tags from current area foreach ( qw/FUNC FLOW OPER STRG CMNT/ ) { $w->tagRemove($_, $s_line.'.0', $e_line.'.end'); } foreach my $ln($s_line .. $e_line) { my $line = $w->get($ln.'.0', $ln.'.end'); # Highlight: strings while ( $line =~ /(" # Start at double quote (?: # For grouping only \\.| # Backslash with any character [^"\\] # Must not be able to find )* # Zero or more sets of those "| (?tagAdd('STRG', $ln.'.'.(pos($line)-length($1)), $ln.'.'.pos($line)); } # Highlight: comments while ( $line =~ /(?tagNames($ln.'.'.(pos($line)-1)) && $w->tagNames($ln.'.'.(pos($line)-1)) eq 'STRG' ); $w->tagAdd('CMNT', $ln.'.'.(pos($line)-1), $ln.'.end'); $line = $w->get($ln.'.0', $ln.'.'.(pos($line)-1)); last; } # Highlight: functions, flow control words and operators, # do not highlight hashes, arrays or scalars while ( $line =~ /(?tagAdd('OPER', $ln.'.'.(pos($line)-length($1)), $ln.'.'.pos($line)); } elsif ( $FLOW{$1} ) { $w->tagAdd('FLOW', $ln.'.'.(pos($line)-length($1)), $ln.'.'.pos($line)); } elsif ( $FUNC{$1} || $1 =~ /^(\d+)$/ ) { $w->tagAdd('FUNC', $ln.'.'.(pos($line)-length($1)), $ln.'.'.pos($line)); } } } } } # END - package TextHighlight ############################################################################### package main; ############################################################################### use File::Find; use File::Basename; use Tk::HList; use Tk::Dialog; use Tk::ROText; use Tk::Balloon; use Tk::DropSite; use Tk::NoteBook; use Tk::Adjuster; # Seed the random number generator BEGIN { srand() if $] < 5.004 } # List of supported file patterns my @filetypes = ( ['Perl Scripts', '.pl', 'TEXT'], ['Perl Modules', '.pm', 'TEXT'], ['Perl CGI Scripts', '.cgi', 'TEXT']); # Create main window and return window handle my $mw = MainWindow->new(-title => 'T-Pad'); # Manage window manager protocol $mw->protocol('WM_DELETE_WINDOW' => \&exitCommand); # Add menubar $mw->configure(-menu => my $menubar = $mw->Menu(-tearoff => $Tk::platform eq 'unix')); # Add 'File' entry to the menu my $file = $menubar->cascade(qw/-label File -underline 0 -menuitems/ => [ [command => '~New', -accelerator => 'Ctrl+N', -command => [\&addPage, 'Untitled']], [command => '~Open...', -accelerator => 'Ctrl+O', -command => \&openDialog], [command => '~Close', -command => \&closeCommand, -state => 'disabled'], '', [command => '~Save', -accelerator => 'Ctrl+S', -command => [\&saveDialog, 's']], [command => 'Save ~As...', -command => [\&saveDialog, 'a']], '', [command => 'E~xit', -command => \&exitCommand], ], -tearoff => $Tk::platform eq 'unix'); # Add 'Edit' entry to the menu my $edit = $menubar->cascade(qw/-label Edit -underline 0 -menuitems/ => [ [command => '~Undo', -accelerator => 'Ctrl+Z', -command => [\&menuCommands, 'eu']], [command => '~Redo', -accelerator => 'Ctrl+Y', -command => [\&menuCommands, 'er']], '', [command => 'Cu~t', -accelerator => 'Ctrl+X', -command => [\&menuCommands, 'et']], [command => 'C~opy', -accelerator => 'Ctrl+C', -command => [\&menuCommands, 'eo']], [command => 'P~aste', -accelerator => 'Ctrl+V', -command => [\&menuCommands, 'ea']], '', [command => 'Select A~ll', -command => [\&menuCommands, 'el']], [command => 'Unsele~ct All',-command => [\&menuCommands, 'ec']], ], -tearoff => $Tk::platform eq 'unix'); # Add 'Misc' entry to the menu my $misc = $menubar->cascade(qw/-label Misc -underline 0 -menuitems/ => [ [command => '~Properties...', -command => \&propertiesDialog], [Checkbutton => 'CR~LF Conversion', -variable => \my $crlf], [command => "Script's PO~D...", -command => \&perlDoc], [command => '~Run', -accelerator => 'Ctrl+R', -command => \&runScript], ], -tearoff => $Tk::platform eq 'unix'); # Add 'Help' entry to the menu my $help = $menubar->cascade(qw/-label Help -underline 0 -menuitems/ => [ [command => '~Commands...', -accelerator => 'F1', -command => \&commandHelp], [command => '~About...', -command => \&aboutDialog], ], -tearoff => $Tk::platform eq 'unix'); # Add NoteBook metaphor my $nb = $mw->NoteBook(); # Accept drops from an external application $nb->DropSite(-dropcommand => \&handleDND, -droptypes => ($^O eq 'MSWin32' or ($^O eq 'cygwin' and $Tk::platform eq 'MSWin32')) ? ['Win32'] : [qw/KDE XDND Sun/]); my ($tw, $cmdHelp, %pageName); # Accept ASCII text file or file which does not exist foreach ( @ARGV ) { if ( (-e $_ && -T _) || !-e _ ) { addPage($_); } } # Add default page if there are no pages in notebook metaphor unless ( keys %pageName ) { addPage('Untitled'); } # Show filename over the 'pageName' using balloons my ($balloon, $msg) = $mw->Balloon(-state => 'balloon', -balloonposition => 'mouse'); $balloon->attach($nb, -balloonmsg => \$msg, -motioncommand => sub { my ($nb, $x, $y) = @_; # Adjust screen to widget coordinates $x -= $nb->rootx; $y -= $nb->rooty; my $name = $nb->identify($x, $y); if ( defined $name ) { $msg = 'File name: '.$pageName{$name}->FileName(); 0; # Don't cancel the balloon } else { 1 } # Cancel the balloon }); # Add status bar to the bottom of the screen my $fr = $mw->Frame->pack(qw/-side bottom -fill x/); $fr->Label(-textvariable => \my $st)->pack(qw/-side left/); $fr->Label(-textvariable => \my $clk)->pack(qw/-side right/); updateClock(); # Create Text widget where the user can type commands my $cw = $mw->Scrolled(qw/Text -spacing2 1 -spacing3 1 -scrollbars e -height 3 -background white -relief ridge/)->pack(qw/-side bottom -fill x -padx 1/); # Miscellaneous configurations to the command window $cw->configure(-font => $tw->cget(-font)); $cw->menu(undef); $cw->tagConfigure('FILE', -foreground => '#0000FF'); $cw->tagBind('FILE', '' => sub { shift->configure(qw/-cursor hand2/); }); $cw->tagBind('FILE', '' => sub { shift->configure(qw/-cursor xterm/); }); $cw->tagBind('FILE', '' => sub { my $text = shift; my ($l) = split(/\./, $text->index('current')); my $txt = $text->get($l.'.0', "$l.end"); if ( $txt =~ /^(.*?)\((\d+)\)/ ) { addPage($1); gotoLine($2); } }); mouseWheel($cw); my $prev_cmd; $mw->Adjuster->packAfter($cw, -side => 'bottom'); $nb->pack(qw/-side top -expand 1 -fill both/); # Arrange for X events to invoke callbacks $cw->bind('', \&executeCommand); $cw->bind('', sub { $tw->focus }); $cw->bind('', \&commandHelp); # Start the GUI and eventloop MainLoop; # Create modal 'About' dialog sub aboutDialog { my $popup = $mw->Dialog( -popover => $mw, -title => 'About T-Pad', -bitmap => 'Tk', -default_button => 'OK', -buttons => ['OK'], -text => "T-Pad\nVersion 4.04 - 05-Aug-2003\n\n". "Copyright (C) Tomi Parviainen\n". "http://www.cpan.org/scripts/\n\n". "Perl Version $]\n". "Tk Version $Tk::VERSION", ); $popup->resizable('no', 'no'); $popup->Show(); } # Add page to notebook metaphor sub addPage { shift if UNIVERSAL::isa($_[0], 'TextHighlight'); my $pageName = shift; # If the page exist, raise the old page and return foreach ( keys %pageName ) { if ( ($pageName{$_})->FileName() eq $pageName && $pageName ne 'Untitled' ) { return $nb->raise($_); } } # Add new page with 'random' name to the notebook my $name = rand(); my $page = $nb->add($name, -label => basename($pageName), -raisecmd => \&pageChanged); # Create a widget with attached scrollbar(s) $tw = $page->Scrolled(qw/TextHighlight -spacing2 1 -spacing3 1 -scrollbars ose -background white -borderwidth 2 -width 80 -height 25 -relief sunken/)->pack(qw/-expand 1 -fill both/); $tw->FileName($pageName); $pageName{$name} = $tw; $tw->bind('', sub { $tw->tagRemove('MTCH', '0.0', 'end'); }); # Change popup menu to contain 'Edit' menu entry $tw->menu($edit->menu); mouseWheel($tw); if ( keys %pageName > 1 ) { # Enable 'File->Close' menu entry $file->cget(-menu)->entryconfigure(2 + ($Tk::platform eq 'unix'), -state => 'normal'); } $nb->raise($name); # Write data to the new page. File 'Untitled' can # be used as a template for new script files! writeData($pageName); # Register callback function $tw->positionChangedCallback(\&updateStatus); } # Remove page and disable 'Close' menu item when needed sub closeCommand { if ( confirmCh() ) { delete $pageName{$nb->raised()}; $nb->delete($nb->raised()); } if ( keys %pageName == 1 ) { # Disable 'File->Close' menu entry $file->cget(-menu)->entryconfigure(2 + ($Tk::platform eq 'unix'), -state => 'disabled'); } } # Confirm the changes user has made before proceeding sub confirmCh { if ( $nb->pagecget($nb->raised(), -label) =~ /\*/ ) { my $answer = $tw->Dialog( -popover => $mw, -text => 'Save changes to '. basename($tw->FileName()), -bitmap => 'warning', -title => 'T-Pad', -default_button => 'Yes', -buttons => [qw/Yes No Cancel/])->Show; if ( $answer eq 'Yes' ) { saveDialog('s'); return 0 if ( $nb->pagecget($nb->raised(), -label) =~ /\*/ || $tw->FileName() eq 'Untitled' ); } elsif ( $answer eq 'Cancel' ) { return 0; } } return 1; } # Create Hierarchical List widget, which shows supported commands # and a short description of each command sub commandHelp { if ( defined $cmdHelp ) { $cmdHelp->focus; return; } $cmdHelp = $mw->Toplevel(-title => 'T-Pad Commands [Ctrl+Tab, ESC]'); my $hl = $cmdHelp->Scrolled('HList', -header => 1, -columns => 2, -scrollbars => 'osoe', -width => 70, -height => 31) ->pack(qw/-expand 1 -fill both/); my %commands = ( 'a' => 'About T-Pad', 'c' => 'Close an opened script file', 'doc x' => 'Look up Perl documentation for built in function \'x\'', 'eval x' => 'Evaluate expression \'x\'', 'f x' => 'Find the specified pattern \'x\'', 'fb x' => 'Find the specified pattern \'x\', proceed backward', 'fc x' => 'Find the specified pattern \'x\', use match case', 'ff o t x' => 'Find the specified pattern \'x\' from folder o, file'. ' types t,'."\n".'Example: ff c:\perl\bin *.pl;*.pm Tk', 'fr x' => 'Find the specified pattern \'x\', use regular expression', 'g x' => 'Goto a specified line \'x\'', 'n' => 'Create a new script file', 'o' => 'Open an existing script file', 'p' => 'File properties', 'r [a]' => 'Run the active script, [with arguments a]', 's' => 'Save the active script using the same filename', 'sa' => 'Save the active script as a new file', 'wc' => 'Change the wrap mode to \'char\'', 'wn' => 'Change the wrap mode to \'none\'', 'ww' => 'Change the wrap mode to \'word\'', 'x' => 'Exit'); my $position = 0; $hl->header('create', 0, -text => 'Command'); $hl->header('create', 1, -text => 'Description'); foreach ( sort keys %commands ) { $hl->add($position, -state => 'disabled'); $hl->itemCreate($position, 0, -text => $_); $hl->itemCreate($position++, 1, -text => $commands{$_}); } $cmdHelp->focus; $cmdHelp->protocol('WM_DELETE_WINDOW' => sub { $cmdHelp->withdraw; undef $cmdHelp; }); } # Execute command given by the user sub executeCommand { my ($ln, $col) = split(/\./, $cw->index('insert')); if ( ($_ = $cw->get(--$ln.'.0', "$ln.end")) eq '' ) { # Repeat previous command $_ = $prev_cmd ? $prev_cmd : ''; $cw->delete($ln--.'.0', 'end'); $cw->insert('end', "\n$_\n"); } else { $prev_cmd = $_ } if ( /^a$/ ) { aboutDialog() } elsif ( /^c$/ ) { if ( keys %pageName > 1 ) { closeCommand(); } } elsif ( /^doc\s+(.+)$/ ) { my $doc = `perldoc -t -f $1`; unless ( $doc ) { $doc = "No documentation for perl function `$1' found"; } $doc =~ s/\n+$//; $cw->insert('end', "$doc\n"); } elsif ( /^eval\s+(.+)$/ ) { eval { my $r = eval $1; $cw->insert('end', ($r ? $r : 'undef')."\n"); }; } elsif ( /^f(.*?)\s+(.+)$/ ) { my ($cm, $da, $no, %sw) = ($1, $2, 0); if ( $cm =~ /c/ ) { $sw{-exact} = 1 } else { $sw{-nocase} = 1 } if ( $cm =~ /r/ ) { $sw{-regexp} = 1 } if ( $cm =~ /b/ ) { $sw{-backwards} = 1 } if ( $tw->tagRanges('MTCH') ) { if ( $sw{-backwards} ) { $tw->markSet('insert', ($tw->tagRanges('MTCH'))[0]); } else { $tw->markSet('insert', ($tw->tagRanges('MTCH'))[1]); } $tw->tagRemove('MTCH', '0.0', 'end'); } if ( $cm =~ /f/ ) { findFiles($da); return; } my $match = $tw->search(keys %sw, -count => \$no, '--', $da, $tw->index('insert')); if ( $match ) { $tw->tagAdd('MTCH', $match, "$match + $no char"); $tw->markSet('insert', "$match + $no char"); $tw->see('insert'); $tw->markUnset('insert'); } else { # Didn't match, ring the bell $mw->bell; } } elsif ( /^g\s*(\d+)$/ ) { gotoLine($1) } elsif ( /^n$/ ) { addPage('Untitled') } elsif ( /^o$/ ) { openDialog() } elsif ( /^p$/ ) { propertiesDialog() } elsif ( /^r\s*(.*)$/ ) { runScript($1) } elsif ( /^s$/ ) { saveDialog('s') } elsif ( /^sa$/ ) { saveDialog('a') } elsif ( /^w([ncw])$/ ) { my $wm = $1; # Wrap mode if ( $wm eq 'n' ) { $wm = 'none' } elsif ( $wm eq 'c' ) { $wm = 'char' } else { $wm = 'word' } $tw->configure(-wrap => $wm); } elsif ( /^x$/ ) { exitCommand() } else { $cw->insert('end', "->[ERROR]\n") } $cw->SetCursor('end-1char'); } # Close all pages and quit T-Pad sub exitCommand { while ( (my $pages = keys %pageName) > 0 ) { closeCommand(); # Check if the user has pressed 'Cancel' button last if ( keys %pageName == $pages ); } exit if ( keys %pageName == 0 ); } # Find '$data' pattern from files, scan recursively sub findFiles { my ($path, $ext, $data) = split(/ /, shift); return unless ( defined $data ); my ($dir, %files); find(sub { return if ( !-f || !-T || !defined $mw->focusCurrent || UNIVERSAL::isa($mw->focusCurrent, 'TextHighlight') ); my $filename = $_; if ( !$dir || $dir ne $File::Find::dir ) { $cw->update; $dir = $File::Find::dir; undef %files; foreach ( split(/;/, $ext) ) { foreach ( glob("$_") ) { $files{$_} = 1; } } } if ( $files{$filename} && open(FILE, $filename) ) { while ( my $line = ) { next unless ( $line =~ /\Q$data\E/i ); chomp($line); $cw->insert('end', "$File::Find::name", 'FILE'); $cw->insert('end', "($.):$line\n"); $cw->SetCursor('end-1char'); $cw->update; } close(FILE) or warn "$!"; } }, $path); } # Goto line, which has been passed as a parameter sub gotoLine { my $line = shift; $tw->markSet('insert', "$line.0"); $tw->see('insert'); $tw->markUnset('insert'); $tw->tagRemove('MTCH', '0.0', 'end'); $tw->tagAdd('MTCH', "$line.0", "$line.0 lineend + 1c"); } # Get the filename of the drop and add new page to the notebook metaphor sub handleDND { my ($sel, $filename) = shift; # In case of an error, do the SelectionGet in an eval block eval { if ( $^O eq 'MSWin32' ) { $filename = $tw->SelectionGet(-selection => $sel, 'STRING'); } else { $filename = $tw->SelectionGet(-selection => $sel, 'FILE_NAME'); } }; if ( defined $filename && -T $filename ) { addPage($filename); } } # Handle different menu accelerator commands, which cannot be handled # directly in menu entry (because of the tight bind of $tw) sub menuCommands { my $cmd = shift; if ( $cmd eq 'eu' ) { $tw->undo } elsif ( $cmd eq 'er' ) { $tw->redo } elsif ( $cmd eq 'et' ) { $tw->clipboardCut } elsif ( $cmd eq 'eo' ) { $tw->clipboardCopy } elsif ( $cmd eq 'ea' ) { $tw->clipboardPaste } elsif ( $cmd eq 'el' ) { $tw->selectAll } elsif ( $cmd eq 'ec' ) { $tw->unselectAll } } # Support for mouse wheel sub mouseWheel { my $w = shift; # Windows support $w->bind('', [sub { $_[0]->yviewScroll(-($_[1]/120)*3, 'units'); }, Tk::Ev('D')]); # UNIX support if ( $Tk::platform eq 'unix' ) { $w->bind('<4>', sub { $_[0]->yviewScroll(-3, 'units') unless $Tk::strictMotif; }); $w->bind('<5>', sub { $_[0]->yviewScroll( 3, 'units') unless $Tk::strictMotif; }); } } # Pop up a dialog box for the user to select a file to open sub openDialog { my $filename = $mw->getOpenFile(-filetypes => \@filetypes); if ( defined $filename and $filename ne '' ) { addPage($filename) } } # Notebook page has changed, change the focus to the new page # and initialise status bar to reflect page data sub pageChanged { $tw = $pageName{$nb->raised()}; $tw->focus if ( !defined $mw->focusCurrent || UNIVERSAL::isa($mw->focusCurrent, 'MainWindow') || UNIVERSAL::isa($mw->focusCurrent, 'TextHighlight') ); # Disable/Enable 'Misc->Properties' menu entry if ( -e $tw->FileName() ) { $misc->cget(-menu)->entryconfigure(0 + ($Tk::platform eq 'unix'), -state => 'active'); } else { $misc->cget(-menu)->entryconfigure(0 + ($Tk::platform eq 'unix'), -state => 'disabled'); } updateStatus(); } # Look up Perl documentation in pod format sub perlDoc { my $fileName = $tw->FileName(); my $doc = `perldoc -t \"$fileName\"`; unless ( $doc ) { $doc = "No documentation found for \"$fileName\".\n"; } my $tl = $mw->Toplevel(-title => "Perl Documentation in POD Format [". (basename($tw->FileName()))."]"); my $pod = $tl->Scrolled(qw/ROText -scrollbars oe -width 80 -height 25 -spacing2 1 -spacing3 1/); mouseWheel($pod); $pod->menu(undef); $pod->configure(-font => $tw->cget(-font)); $pod->pack(qw/-expand 1 -fill both/); $pod->insert('1.0', $doc); $pod->focus; } # Create modal 'Properties' dialog sub propertiesDialog { # Return if the file does not exist return unless ( -e $tw->FileName() ); my $popup = $mw->Dialog( -popover => $mw, -title => 'Source File Properties', -bitmap => 'info', -default_button => 'OK', -buttons => ['OK'], -text => "Name:\t".basename($tw->FileName()). "\nSize:\t".(stat($tw->FileName()))[7]." Bytes\n". "Saved:\t".localtime((stat($tw->FileName()))[9])."\n". "Mode:\t".sprintf("%04o", 07777&(stat($tw->FileName()))[2]) ); $popup->resizable('no', 'no'); $popup->Show(); } # Run the script (currently with blocking the caller) sub runScript { shift if UNIVERSAL::isa($_[0], 'TextHighlight'); my $params = $_[0] ? $_[0] : ''; if ( confirmCh() && -e $tw->FileName() ) { system("$^X \"".$tw->FileName()."\" $params"); } } # Pop up a dialog box for the user to select a file to save sub saveDialog { my $filename; shift if UNIVERSAL::isa($_[0], 'TextHighlight'); if ( $_[0] eq 's' && $tw->FileName() ne 'Untitled' ) { $filename = $tw->FileName(); } else { $filename = $mw->getSaveFile(-filetypes => \@filetypes, -initialfile => basename($tw->FileName()), -defaultextension => '.pl'); } if ( defined $filename and $filename ne '' ) { if ( open(FILE, ">$filename") ) { # Write file to disk (change cursor to reflect this operation) $mw->Busy(-recurse => 1); my ($e_line) = split(/\./, $tw->index('end - 1 char')); foreach ( 1 .. $e_line-1 ) { print FILE $tw->get($_.'.0', $_.'.0 + 1 lines'); } print FILE $tw->get($e_line.'.0', 'end - 1 char'); $mw->Unbusy; close(FILE) or print "$!"; $tw->FileName($filename); $nb->pageconfigure($nb->raised(), -label => basename($filename)); $tw->numberChangesExt($tw->numberChanges); # Ensure 'File->Properties' menu entry is active $misc->cget(-menu)->entryconfigure(0 + ($Tk::platform eq 'unix'), -state => 'active'); } else { my $msg = "File may be ReadOnly, or open for write by ". "another application! Use 'Save As' to save ". "as a different name."; $mw->Dialog(-popover => $mw, -text => $msg, -bitmap => 'warning', -title => 'Cannot save file', -buttons => ['OK'])->Show; } } } # Update clock (without seconds) every minute sub updateClock { ($clk = scalar localtime) =~ s/(\d+:\d+):(\d+)\s/$1 /; $mw->after((60-$2)*1000, \&updateClock); } # Update the statusbar sub updateStatus { my ($cln, $ccol) = split(/\./, $tw->index('insert')); my ($lln) = split(/\./, $tw->index('end')); $st = "Line $cln (".($lln-1).'), Column '.($ccol+1); my $title = $nb->pagecget($nb->raised(), -label); # Check do we need to add/remove '*' from title if ( $tw->numberChanges != $tw->numberChangesExt() ) { if ( $title !~ /\*/ ) { $title .= '*'; $nb->pageconfigure($nb->raised(), -label => $title); } } elsif ( $title =~ /\*/ ) { $title =~ s/\*//; $nb->pageconfigure($nb->raised(), -label => $title); } } # Write data to text widget via read buffer sub writeData { my $filename = $tw->FileName(); if ( -e $filename ) { open(FILE, $filename) or die "$!"; my $read_buffer; while ( ) { s/\x0D?\x0A/\n/ if ( $crlf ); $read_buffer .= $_; if ( ($.%100) == 0 ) { $tw->insertWHL('end', $read_buffer); undef $read_buffer; } } if ( $read_buffer ) { $tw->insertWHL('end', $read_buffer); } close(FILE) or die "$!"; } $tw->ResetUndo; # Set cursor to the first line of text widget $tw->insertWHL('0.0'); $tw->backgroundHL(1); } __END__ =head1 NAME T-Pad - A Perl/Tk GUI based Perl-script editor with syntax highlighting =head1 SYNOPSIS perl B [I] =head1 DESCRIPTION T-Pad is a Perl/Tk GUI based text editor with syntax highlight. T-Pad supports syntax highlight for *.pl, *.pm and *.cgi -files. It contains a command window to where a user can type commands to test for example a functionality of regular expression, evaluate Perl's predefined variables, look up documentation for built in functions etc. =head1 README A Perl/Tk GUI based Perl-script editor with syntax highlighting (*.pl, *.pm and *.cgi). T-Pad runs under Windows, Unix and Linux. =head1 PREREQUISITES This script requires the C a graphical user interface toolkit module for Perl. =head1 AUTHOR Tomi Parviainen > =head1 COPYRIGHT Copyright (c) 2002-2003, Tomi Parviainen. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =pod SCRIPT CATEGORIES Win32 Win32/Utilities =cut