diff options
author | ribet | 2007-03-21 10:19:39 +0000 |
---|---|---|
committer | ribet | 2007-03-21 10:19:39 +0000 |
commit | c5866f304210618979d03c561b1e3f6f83200bce (patch) | |
tree | 7c81ae161f78cdf952f3d3a33184f8bf322c9bd8 /src/MTools/ptkdb.pm | |
parent | a023d10b564d8c29566304f7777b4ec87c5b7b4d (diff) | |
download | mtc-c5866f304210618979d03c561b1e3f6f83200bce.zip mtc-c5866f304210618979d03c561b1e3f6f83200bce.tar.gz mtc-c5866f304210618979d03c561b1e3f6f83200bce.tar.bz2 mtc-c5866f304210618979d03c561b1e3f6f83200bce.tar.xz |
Import initial
Diffstat (limited to 'src/MTools/ptkdb.pm')
-rw-r--r-- | src/MTools/ptkdb.pm | 4229 |
1 files changed, 4229 insertions, 0 deletions
diff --git a/src/MTools/ptkdb.pm b/src/MTools/ptkdb.pm new file mode 100644 index 0000000..5962e63 --- /dev/null +++ b/src/MTools/ptkdb.pm @@ -0,0 +1,4229 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU LGPL Libray General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU Library General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA, +# or refer to http://www.gnu.org/copyleft/lgpl.html +# +################################################################## + +package DB ; + +## +## Expedient fix for perl 5.8.0. True DB::DB is further down. +## +## +sub DB {} + +use Tk ; + +# +# If you've loaded this file via a browser +# select "Save As..." from your file menu +# +# ptkdb Perl Tk perl Debugger +# +# Copyright 1998, Andrew E. Page +# All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of either: +# +# a) the GNU General Public License as published by the Free +# Software Foundation; either version 1, or (at your option) any +# later version, or +# +# b) the "Artistic License" which comes with this Kit. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either +# the GNU General Public License or the Artistic License for more details. +# + + +#################################### +### Sample .Xresources for ptkdb ### +#################################### +# /* +# * Perl Tk Debugger XResources. +# * Note... These resources are subject to change. +# * +# * Use 'xfontsel' to select different fonts. +# * +# * Append these resource to ~/.Xdefaults | ~/.Xresources +# * and use xrdb -override ~/.Xdefaults | ~/.Xresources +# * to activate them. +# */ +# /* Set Value to se to place scrollbars on the right side of windows +# CAUTION: extra whitespace at the end of the line is causing +# failures with Tk800.011. +# */ +# ptkdb*scrollbars: sw +# +# /* controls where the code pane is oriented, down the left side, or across the top */ +# /* values can be set to left, right, top, bottom */ +# ptkdb*codeside: left +# /* +# * Background color for the balloon +# * CAUTION: For certain versions of Tk trailing +# * characters after the color produces an error +# */ +# ptkdb.frame2.frame1.rotext.balloon.background: green +# ptkdb.frame2.frame1.rotext.balloon.font: fixed /* Hot Variable Balloon Font */ +# +# +# ptkdb.frame*font: fixed /* Menu Bar */ +# ptkdb.frame.menubutton.font: fixed /* File menu */ +# ptkdb.frame2.frame1.rotext.font: fixed /* Code Pane */ +# ptkdb.notebook.datapage.frame1.hlist.font: fixed /* Expression Notebook Page */ +# +# ptkdb.notebook.subspage*font: fixed /* Subroutine Notebook Page */ +# ptkdb.notebook.brkptspage*entry.font: fixed /* Delete Breakpoint Buttons */ +# ptkdb.notebook.brkptspage*button.font: fixed /* Breakpoint Expression Entries */ +# ptkdb.notebook.brkptspage*button1.font: fixed /* Breakpoint Expression Entries */ +# ptkdb.notebook.brkptspage*checkbutton.font: fixed /* Breakpoint Checkbuttons */ +# ptkdb.notebook.brkptspage*label.font: fixed /* Breakpoint "Cond" label */ +# +# ptkdb.toplevel.frame.textundo.font: fixed /* Eval Expression Entry Window */ +# ptkdb.toplevel.frame1.text.font: fixed /* Eval Expression Results Window */ +# ptkdb.toplevel.button.font: fixed /* "Eval..." Button */ +# ptkdb.toplevel.button1.font: fixed /* "Clear Eval" Button */ +# ptkdb.toplevel.button2.font: fixed /* "Clear Results" Button */ +# ptkdb.toplevel.button3.font: fixed /* "Clear Dismiss" Button */ +# +# +# /* +# * Background color for where the debugger has stopped +# */ +# ptkdb*stopcolor: blue +# +# /* +# * Background color for set breakpoints +# */ +# ptkdb*breaktagcolor: red +# +# /* +# * Font for where the debugger has stopped +# */ +# ptkdb*stopfont: -*-fixed-bold-*-*-*-*-*-*-*-*-*-*-* +# +# /* +# * Background color for the search tag +# */ +# ptkdb*searchtagcolor: green + +use strict ; +use vars qw($VERSION @dbline %dbline); + +# +# This package is the main_window object +# for the debugger. We start with the Devel:: +# prefix because we want to install it with +# the DB:: package that is required to be in a Devel/ +# subdir of a directory in the @INC set. +# +package Devel::ptkdb ; + +=head1 NAME + +Devel::ptkdb - Perl debugger using a Tk GUI + +=head1 DESCRIPTION + + ptkdb is a debugger for perl that uses perlTk for a user interface. + Features include: + + Hot Variable Inspection + Breakpoint Control Panel + Expression List + Subroutine Tree + + +=begin html + + <body bgcolor=white> + +=end html + +=head1 SYNOPSIS + +To debug a script using ptkdb invoke perl like this: + + perl -d:ptkdb myscript.pl + +=head1 Usage + + perl -d:ptkdb myscript.pl + +=head1 Code Pane + +=over 4 + +=item Line Numbers + + Line numbers are presented on the left side of the window. Lines that + have lines through them are not breakable. Lines that are plain text + are breakable. Clicking on these line numbers will insert a + breakpoint on that line and change the line number color to + $ENV{'PTKDB_BRKPT_COLOR'} (Defaults to Red). Clicking on the number + again will remove the breakpoint. If you disable the breakpoint with + the controls on the BrkPt notebook page the color will change to + $ENV{'PTKDB_DISABLEDBRKPT_COLOR'}(Defaults to Green). + +=item Cursor Motion + +If you place the cursor over a variable (i.e. $myVar, @myVar, or +%myVar) and pause for a second the debugger will evaluate the current +value of the variable and pop a balloon up with the evaluated +result. I<This feature is not available with Tk400.> + +If Data::Dumper(standard with perl5.00502)is available it will be used +to format the result. If there is an active selection, the text of +that selection will be evaluated. + +=back + +=head1 Notebook Pane + +=over 2 + +=item Exprs + + This is a list of expressions that are evaluated each time the + debugger stops. The results of the expresssion are presented + heirarchically for expression that result in hashes or lists. Double + clicking on such an expression will cause it to collapse; double + clicking again will cause the expression to expand. Expressions are + entered through B<Enter Expr> entry, or by Alt-E when text is + selected in the code pane. + + The B<Quick Expr> entry, will take an expression, evaluate it, and + replace the entries contents with the result. The result is also + transfered to the 'clipboard' for pasting. + +=item Subs + + Displays a list of all the packages invoked with the script + heirarchially. At the bottom of the heirarchy are the subroutines + within the packages. Double click on a package to expand + it. Subroutines are listed by their full package names. + +=item BrkPts + + Presents a list of the breakpoints current in use. The pushbutton + allows a breakpoint to be 'disabled' without removing it. Expressions + can be applied to the breakpoint. If the expression evaluates to be + 'true'(results in a defined value that is not 0) the debugger will + stop the script. Pressing the 'Goto' button will set the text pane + to that file and line where the breakpoint is set. Pressing the + 'Delete' button will delete the breakpoint. + +=back + +=head1 Menus + +=head2 File Menu + +=over + +=item About... + +Presents a dialog box telling you about the version of ptkdb. It +recovers your OS name, version of perl, version of Tk, and some other +information + +=item Open + +Presents a list of files that are part of the invoked perl +script. Selecting a file from this list will present this file in the +text window. + +=item Save Config... + +Requires Data::Dumper. Prompts for a filename to save the +configuration to. Saves the breakpoints, expressions, eval text and +window geometry. If the name given as the default is used and the +script is reinvoked, this configuration will be reloaded +automatically. + + B<NOTE:> You may find this preferable to using + +=item Restore Config... + +Requires Data::Dumper. Prompts for a filename to restore a configuration saved with +the "Save Config..." menu item. + +=item Goto Line... + +Prompts for a line number. Pressing the "Okay" button sends the window to the line number entered. +item Find Text... + +Prompts for text to search for. Options include forward search, +backwards search, and regular expression searching. + +=item Quit + + Causes the debugger and the target script to exit. + +=back + +=head2 Control Menu + +=over + +=item Run + +The debugger allows the script to run to the next breakpoint or until the script exits. +item Run To Here + +Runs the debugger until it comes to wherever the insertion cursor +in text window is placed. + +=item Set Breakpoint + +Sets a breakpoint on the line at the insertion cursor. +item Clear Breakpoint + +Remove a breakpoint on the at the insertion cursor. + +=item Clear All Breakpoints + +Removes all current breakpoints + +=item Step Over + +Causes the debugger to step over the next line. If the line is a +subroutine call it steps over the call, stopping when the subroutine +returns. + +=item Step In + +Causes the debugger to step into the next line. If the line is a +subroutine call it steps into the subroutine, stopping at the first +executable line within the subroutine. + +=item Return + +Runs the script until it returns from the currently executing +subroutine. + +=item Restart + +Saves the breakpoints and expressions in a temporary file and restarts +the script from the beginning. CAUTION: This feature will not work +properly with debugging of CGI Scripts. + +=item Stop On Warning + +When C<-w> is enabled the debugger will stop when warnings such as, "Use +of uninitialized value at undef_warn.pl line N" are encountered. The debugger +will stop on the NEXT line of execution since the error can't be detected +until the current line has executed. + +This feature can be turned on at startup by adding: + +$DB::ptkdb::stop_on_warning = 1 ; + +to a .ptkdbrc file + +=back + +=head2 Data Menu + +=over + +=item Enter Expression + +When an expression is entered in the "Enter Expression:" text box, +selecting this item will enter the expression into the expression +list. Each time the debugger stops this expression will be evaluated +and its result updated in the list window. + +=item Delete Expression + + Deletes the highlighted expression in the expression window. + +=item Delete All Expressions + + Delete all expressions in the expression window. + +=item Expression Eval Window + +Pops up a two pane window. Expressions of virtually unlimitted length +can be entered in the top pane. Pressing the 'Eval' button will cause +the expression to be evaluated and its placed in the lower pane. If +Data::Dumper is available it will be used to format the resulting +text. Undo is enabled for the text in the upper pane. + +HINT: You can enter multiple expressions by separating them with commas. + +=item Use Data::Dumper for Eval Window + +Enables or disables the use of Data::Dumper for formatting the results +of expressions in the Eval window. + +=back + +=head2 Stack Menu + +Maintains a list of the current subroutine stack each time the +debugger stops. Selecting an item from this menu will set the text in +the code window to that particular subourtine entry point. + +=head2 Bookmarks Menu + +Maintains a list of bookmarks. The booksmarks are saved in ~/.ptkdb_bookmarks + +=over + +=item Add Bookmark + +Adds a bookmark to the bookmark list. + +=back + +=head1 Options + +Here is a list of the current active XResources options. Several of +these can be overridden with environmental variables. Resources can be +added to .Xresources or .Xdefaults depending on your X configuration. +To enable these resources you must either restart your X server or use +the xrdb -override resFile command. xfontsel can be used to select +fonts. + + /* + * Perl Tk Debugger XResources. + * Note... These resources are subject to change. + * + * Use 'xfontsel' to select different fonts. + * + * Append these resource to ~/.Xdefaults | ~/.Xresources + * and use xrdb -override ~/.Xdefaults | ~/.Xresources + * to activate them. + */ + /* Set Value to se to place scrollbars on the right side of windows + CAUTION: extra whitespace at the end of the line is causing + failures with Tk800.011. + + sw -> puts scrollbars on left, se puts scrollars on the right + + */ + ptkdb*scrollbars: sw + /* controls where the code pane is oriented, down the left side, or across the top */ + /* values can be set to left, right, top, bottom */ + ptkdb*codeside: left + + /* + * Background color for the balloon + * CAUTION: For certain versions of Tk trailing + * characters after the color produces an error + */ + ptkdb.frame2.frame1.rotext.balloon.background: green + ptkdb.frame2.frame1.rotext.balloon.font: fixed /* Hot Variable Balloon Font */ + + + ptkdb.frame*font: fixed /* Menu Bar */ + ptkdb.frame.menubutton.font: fixed /* File menu */ + ptkdb.frame2.frame1.rotext.font: fixed /* Code Pane */ + ptkdb.notebook.datapage.frame1.hlist.font: fixed /* Expression Notebook Page */ + + ptkdb.notebook.subspage*font: fixed /* Subroutine Notebook Page */ + ptkdb.notebook.brkptspage*entry.font: fixed /* Delete Breakpoint Buttons */ + ptkdb.notebook.brkptspage*button.font: fixed /* Breakpoint Expression Entries */ + ptkdb.notebook.brkptspage*button1.font: fixed /* Breakpoint Expression Entries */ + ptkdb.notebook.brkptspage*checkbutton.font: fixed /* Breakpoint Checkbuttons */ + ptkdb.notebook.brkptspage*label.font: fixed /* Breakpoint Checkbuttons */ + + ptkdb.toplevel.frame.textundo.font: fixed /* Eval Expression Entry Window */ + ptkdb.toplevel.frame1.text.font: fixed /* Eval Expression Results Window */ + ptkdb.toplevel.button.font: fixed /* "Eval..." Button */ + ptkdb.toplevel.button1.font: fixed /* "Clear Eval" Button */ + ptkdb.toplevel.button2.font: fixed /* "Clear Results" Button */ + ptkdb.toplevel.button3.font: fixed /* "Clear Dismiss" Button */ + + /* + * Background color for where the debugger has stopped + */ + ptkdb*stopcolor: blue + + /* + * Background color for set breakpoints + */ + ptkdb*breaktagcolor*background: yellow + ptkdb*disabledbreaktagcolor*background: white + /* + * Font for where the debugger has stopped + */ + ptkdb*stopfont: -*-fixed-bold-*-*-*-*-*-*-*-*-*-*-* + + /* + * Background color for the search tag + */ + ptkdb*searchtagcolor: green + +=head1 Environmental Variables + +=over 4 + +=item PTKDB_BRKPT_COLOR + +Sets the background color of a set breakpoint + +=item PTKDB_DISABLEDBRKPT_COLOR + +Sets the background color of a disabled breakpoint + +=item PTKDB_CODE_FONT + +Sets the font of the Text in the code pane. + +=item PTKDB_CODE_SIDE + +Sets which side the code pane is packed onto. Defaults to 'left'. +Can be set to 'left', 'right', 'top', 'bottom'. + +Overrides the Xresource ptkdb*codeside: I<side>. + +=item PTKDB_EXPRESSION_FONT + + Sets the font used in the expression notebook page. + +=item PTKDB_EVAL_FONT + + Sets the font used in the Expression Eval Window + +=item PTKDB_EVAL_DUMP_INDENT + + Sets the value used for Data::Dumper 'indent' setting. See man Data::Dumper + +=item PTKDB_SCROLLBARS_ONRIGHT + + A non-zero value Sets the scrollbars of all windows to be on the + right side of the window. Useful for Windows users using ptkdb in an + XWindows environment. + +=item PTKDB_LINENUMBER_FORMAT + +Sets the format of line numbers on the left side of the window. Default value is %05d. useful +if you have a script that contains more than 99999 lines. + +=item PTKDB_DISPLAY + +Sets the X display that the ptkdb window will appear on when invoked. Useful for debugging CGI +scripts on remote systems. + +=item PTKDB_BOOKMARKS_PATH + +Sets the path of the bookmarks file. Default is $ENV{'HOME'}/.ptkdb_bookmarks + +=item PTKDB_STOP_TAG_COLOR + +Sets the color that highlights the line where the debugger is stopped + +=back + +=head1 FILES + +=head2 .ptkdbrc + +If this file is present in ~/ or in the directory where perl is +invoked the file will be read and executed as a perl script before the +debugger makes its initial stop at startup. There are several 'api' +calls that can be used with such scripts. There is an internal +variable $DB::no_stop_at_start that may be set to non-zero to prevent +the debugger from stopping at the first line of the script. This is +useful for debugging CGI scripts. + +There is a system ptkdbrc file in $PREFIX/lib/perl5/$VERS/Devel/ptkdbrc + +=over 4 + +=item brkpt($fname, @lines) + +Sets breakspoints on the list of lines in $fname. A warning message +is generated if a line is not breakable. + +=item condbrkpt($fname, @($line, $expr) ) + +Sets conditional breakpoints in $fname on pairs of $line and $expr. A +warning message is generated if a line is not breakable. NOTE: the +validity of the expression will not be determined until execution of +that particular line. + +=item brkonsub(@names) + +Sets a breakpoint on each subroutine name listed. A warning message is +generated if a subroutine does not exist. NOTE: for a script with no +other packages the default package is "main::" and the subroutines +would be "main::mySubs". + +=item brkonsub_regex(@regExprs) + +Uses the list of @regExprs as a list of regular expressions to set breakpoints. Sets breakpoints +on every subroutine that matches any of the listed regular expressions. + +=item textTagConfigure(tag, ?option?, ?value?) + +Allows the user to format the text in the code window. The option +value pairs are the same values as the option for the tagConfigure +method documented in Tk::Text. Currently the following tags are in +effect: + + + 'code' Format for code in the text pane + 'stoppt' Format applied to the line where the debugger is currently stopped + 'breakableLine' Format applied to line numbers where the code is 'breakable' + 'nonbreakableLine' Format applied to line numbers where the code is no breakable + 'breaksetLine' Format applied to line numbers were a breakpoint is set + 'breakdisabledLine' Format applied to line numbers were a disabled breakpoint is set + 'search_tag' Format applied to text when located by a search. + + Example: + + # + # Turns off the overstrike on lines that you can't set a breakpoint on + # and makes the text color yellow. + # + textTagConfigure('nonbreakableLine', -overstrike => 0, -foreground => "yellow") ; + +=item add_exprs(@exprList) + +Add a list of expressions to the 'Exprs' window. NOTE: use the single +quote character \' to prevent the expression from being "evaluated" in +the string context. + + + Example: + + # + # Adds the $_ and @_ expressions to the active list + # + + add_exprs('$_', '@_') ; + +=back + +=head1 NOTES + +=head2 Debugging Other perlTk Applications + +ptkdb can be used to debug other perlTk applications if some cautions +are observed. Basically, do not click the mouse in the application's +window(s) when you've entered the debugger and do not click in the +debugger's window(s) while the application is running. Doing either +one is not necessarily fatal, but it can confuse things that are going +on and produce unexpected results. + +Be aware that most perlTk applications have a central event loop. +User actions, such as mouse clicks, key presses, window exposures, etc +will generate 'events' that the script will process. When a perlTk +application is running, its 'MainLoop' call will accept these events +and then dispatch them to appropriate callbacks associated with the +appropriate widgets. + +Ptkdb has its own event loop that runs whenever you've stopped at a +breakpoint and entered the debugger. However, it can accept events +that are generated by other perlTk windows and dispatch their +callbacks. The problem here is that the application is supposed to be +'stopped', and logically the application should not be able to process +events. + +A future version of ptkdb will have an extension that will 'filter' +events so that application events are not processed while the debugger +is active, and debugger events will not be processed while the target +script is active. + +=head2 Debugging CGI Scripts + +One advantage of ptkdb over the builtin debugger(-d) is that it can be +used to debug CGI perl scripts as they run on a web server. Be sure +that that your web server's perl instalation includes Tk. + +Change your + + #! /usr/local/bin/perl + +to + + #! /usr/local/bin/perl -d:ptkdb + +TIP: You can debug scripts remotely if you're using a unix based +Xserver and where you are authoring the script has an Xserver. The +Xserver can be another unix workstation, a Macintosh or Win32 platform +with an appropriate XWindows package. In your script insert the +following BEGIN subroutine: + + sub BEGIN { + $ENV{'DISPLAY'} = "myHostname:0.0" ; + } + +Be sure that your web server has permission to open windows on your +Xserver (see the xhost manpage). + +Access your web page with your browswer and 'submit' the script as +normal. The ptkdb window should appear on myHostname's monitor. At +this point you can start debugging your script. Be aware that your +browser may timeout waiting for the script to run. + +To expedite debugging you may want to setup your breakpoints in +advance with a .ptkdbrc file and use the $DB::no_stop_at_start +variable. NOTE: for debugging web scripts you may have to have the +.ptkdbrc file installed in the server account's home directory (~www) +or whatever username your webserver is running under. Also try +installing a .ptkdbrc file in the same directory as the target script. + +=head1 KNOWN PROBLEMS + +=over + +=item I<Breakpoint Controls> + +If the size of the right hand pane is too small the breakpoint controls +are not visible. The breakpoints are still there, the window may have +to be enlarged in order for them to be visible. + +=item Balloons and Tk400 + +The Balloons in Tk400 will not work with ptkdb. All other functions +are supported, but the Balloons require Tk800 or higher. + +=back + +=head1 AUTHOR + +Andrew E. Page, aep@world.std.com + +=head1 ACKNOWLEDGEMENTS + +Matthew Persico For suggestions, and beta testing. + +=cut + + +require 5.004 ; + + +## +## Perform a check to see if we have the Tk library, if not, attempt +## to load it for the user +## + +sub BEGIN { + +eval { +require Tk ; +} ; +if( $@ ) { +print << "__PTKDBTK_INSTALL__" ; +*** +*** The PerlTk library could not be found. Ptkdb requires the PerlTk library. +*** +Preferably Tk800.015 or better: + +In order to install this the following conditions must be met: + +1. You have to have access to a C compiler. +2. You must have sufficient permissions to install the libraries on your system. + +To install PerlTk: + +a Download the Tk library source from http://www.perl.com/CPAN/modules/by-category/08_User_Interfaces/Tk +b Uncompress the archive and run "perl Makefile.PL" +c run "make install" + + If this process completes successfully ptkdb should be operational now. + +We can attempt to run the CPAN module for you. This will, after some questions, download +and install the Tk library automatically. + +Would you like to run the CPAN module? (y/n) +__PTKDBTK_INSTALL__ + +my $answer = <STDIN> ; +chomp $answer ; +if( $answer =~ /y|yes/i) { + require CPAN ; + CPAN::install Tk ; +} # if + +} # if $@ + + +} # end of sub BEGIN + +use Tk 800 ; +use Data::Dumper ; + +require Tk::Dialog; +require Tk::TextUndo ; +require Tk::ROText; +require Tk::NoteBook ; +require Tk::HList ; +require Tk::Table ; + +use vars qw(@dbline) ; + +use Config ; +# +# Check to see if the package actually +# exists. If it does import the routines +# and return a true value ; +# +# NOTE: this needs to be above the 'BEGIN' subroutine, +# otherwise it will not have been compiled by the time +# that it is called by sub BEGIN. +# +sub check_avail { + my ($mod, @list) = @_ ; + + eval { + require $mod ; import $mod @list ; + } ; + + return 0 if $@ ; + return 1 ; + +} # end of check_avail + +sub BEGIN { + + $DB::on = 0 ; + + $DB::subroutine_depth = 0 ; # our subroutine depth counter + $DB::step_over_depth = -1 ; + + # + # the bindings and font specs for these operations have been placed here + # to make them accessible to people who might want to customize the + # operations. REF The 'bind.html' file, included in the perlTk FAQ has + # a fairly good explanation of the binding syntax. + # + + # + # These lists of key bindings will be applied + # to the "Step In", "Step Out", "Return" Commands + # + $Devel::ptkdb::pathSep = '\x00' ; + $Devel::ptkdb::pathSepReplacement = "\0x01" ; + + @Devel::ptkdb::step_in_keys = ( '<Shift-F9>', '<Alt-s>', '<Button-3>' ) ; # step into a subroutine + @Devel::ptkdb::step_over_keys = ( '<F9>', '<Alt-n>', '<Shift-Button-3>' ) ; # step over a subroutine + @Devel::ptkdb::return_keys = ( '<Alt-u>', '<Control-Button-3>' ) ; # return from a subroutine + @Devel::ptkdb::toggle_breakpt_keys = ( '<Alt-b>' ) ; # set or unset a breakpoint + + # Fonts used in the displays + + # + # NOTE: The environmental variable syntax here works like this: + # $ENV{'NAME'} accesses the environmental variable "NAME" + # + # $ENV{'NAME'} || 'string' results in $ENV{'NAME'} or 'string' if $ENV{'NAME'} is not defined. + # + # + + @Devel::ptkdb::button_font = $ENV{'PTKDB_BUTTON_FONT'} ? ( "-font" => $ENV{'PTKDB_CODE_FONT'} ) : () ; # font for buttons + @Devel::ptkdb::code_text_font = $ENV{'PTKDB_CODE_FONT'} ? ( "-font" => $ENV{'PTKDB_CODE_FONT'} ) : () ; + + @Devel::ptkdb::expression_text_font = $ENV{'PTKDB_EXPRESSION_FONT'} ? ( "-font" => $ENV{'PTKDB_EXPRESSION_FONT'} ) : () ; + @Devel::ptkdb::eval_text_font = $ENV{'PTKDB_EVAL_FONT'} ? ( -font => $ENV{'PTKDB_EVAL_FONT'} ) : () ; # text for the expression eval window + + $Devel::ptkdb::eval_dump_indent = $ENV{'PTKDB_EVAL_DUMP_INDENT'} || 1 ; + + # + # Windows users are more used to having scroll bars on the right. + # If they've set PTKDB_SCROLLBARS_ONRIGHT to a non-zero value + # this will configure our scrolled windows with scrollbars on the right + # + # this can also be done by setting: + # + # ptkdb*scrollbars: se + # + # in the .Xdefaults/.Xresources file on X based systems + # + if( exists $ENV{'PTKDB_SCROLLBARS_ONRIGHT'} && $ENV{'PTKDB_SCROLLBARS_ONRIGHT'} ) { + @Devel::ptkdb::scrollbar_cfg = ('-scrollbars' => 'se') ; + } + else { + @Devel::ptkdb::scrollbar_cfg = ( ) ; + } + + # + # Controls how far an expression result will be 'decomposed'. Setting it + # to 0 will take it down only one level, setting it to -1 will make it + # decompose it all the way down. However, if you have a situation where + # an element is a ref back to the array or a root of the array + # you could hang the debugger by making it recursively evaluate an expression + # + $Devel::ptkdb::expr_depth = -1 ; + $Devel::ptkdb::add_expr_depth = 1 ; # how much further to expand an expression when clicked + + $Devel::ptkdb::linenumber_format = $ENV{'PTKDB_LINENUMBER_FORMAT'} || "%05d " ; + $Devel::ptkdb::linenumber_length = 5 ; + + $Devel::ptkdb::linenumber_offset = length sprintf($Devel::ptkdb::linenumber_format, 0) ; + $Devel::ptkdb::linenumber_offset -= 1 ; + + # + # Check to see if "Data Dumper" is available + # if it is we can save breakpoints and other + # various "functions". This call will also + # load the subroutines needed. + # + $Devel::ptkdb::DataDumperAvailable = 1 ; # assuming that it is now + $Devel::ptkdb::useDataDumperForEval = $Devel::ptkdb::DataDumperAvailable ; + + # + # DB Options (things not directly involving the window) + # + + # Flag to disable us from intercepting $SIG{'INT'} + + $DB::sigint_disable = defined $ENV{'PTKDB_SIGINT_DISABLE'} && $ENV{'PTKDB_SIGINT_DISABLE'} ; +# +# Possibly for debugging perl CGI Web scripts on +# remote machines. +# + $ENV{'DISPLAY'} = $ENV{'PTKDB_DISPLAY'} if exists $ENV{'PTKDB_DISPLAY'} ; + + } # end of BEGIN + +sub DESTROY { + my ($self) = @_ ; + + $self->save_bookmarks($self->{BookMarksPath}) if $Devel::ptkdb::DataDumperAvailable && $self->{'bookmarks_changed'}; + + +} # end of ptkdb::DESTROY + +## +## subroutine provided to the user for initializing +## files in .ptkdbrc +## +sub brkpt { + my ($fName, @idx) = @_ ; + my($offset) ; + local(*dbline) = $main::{'_<' . $fName} ; + + $offset = $dbline[1] =~ /use\s+.*Devel::_?ptkdb/ ? 1 : 0 ; + + for( @idx ) { + if( !&DB::checkdbline($fName, $_ + $offset) ) { + my ($package, $filename, $line) = caller ; + print "$filename:$line: $fName line $_ is not breakable\n" ; + next ; + } + $DB::window->insertBreakpoint($fName, $_, 1) ; # insert a simple breakpoint + } +} # end of brkpt + +# +# Set conditional breakpoint(s) +# +sub condbrkpt { + my ($fname) = shift ; + my($offset) ; + local(*dbline) = $main::{'_<' . $fname} ; + + $offset = $dbline[1] =~ /use\s+.*Devel::_?ptkdb/ ? 1 : 0 ; + + while( @_ ) { # arg loop + my($index, $expr) = splice @_, 0, 2 ; # take args 2 at a time + + if( !&DB::checkdbline($fname, $index + $offset) ) { + my ($package, $filename, $line) = caller ; + print "$filename:$line: $fname line $index is not breakable\n" ; + next ; + } + $DB::window->insertBreakpoint($fname, $index, 1, $expr) ; # insert a simple breakpoint + } # end of arg loop + +} # end of conditionalbrkpt + +sub brkonsub { + my(@names) = @_ ; + + for( @names ) { + + # get the filename and line number range of the target subroutine + + if( !exists $DB::sub{$_} ) { + print "No subroutine $_. Try main::$_\n" ; + next ; + } + + $DB::sub{$_} =~ /(.*):([0-9]+)-([0-9]+)$/o ; # file name will be in $1, start line $2, end line $3 + + for( $2..$3 ) { + next unless &DB::checkdbline($1, $_) ; + $DB::window->insertBreakpoint($1, $_, 1) ; + last ; # only need the one breakpoint + } + } # end of name loop + +} # end of brkonsub + +# +# set breakpoints on subroutines matching a regular +# expression +# +sub brkonsub_regex { + my(@regexps) = @_ ; + my($regexp, @subList) ; + + # + # accumulate matching subroutines + # + foreach $regexp ( @regexps ) { + study $regexp ; + push @subList, grep /$regexp/, keys %DB::sub ; + } # end of brkonsub_regex + + brkonsub(@subList) ; # set breakpoints on matching subroutines + +} # end of brkonsub_regex + +# +# Allow the user Access to our tag configurations +# +sub textTagConfigure { + my ($tag, @config) = @_ ; + + $DB::window->{'text'}->tagConfigure($tag, @config) ; + +} # end of textTagConfigure + +## +## Change the tabs in the text field +## +sub setTabs { + + $DB::window->{'text'}->configure(-tabs => [ @_ ]) ; + +} + +# +# User .ptkdbrc API +# allows the user to add expressions to +# the expression list window. +# +sub add_exprs { + push @{$DB::window->{'expr_list'}}, map { 'expr' => $_, 'depth' => $Devel::ptkdb::expr_depth }, @_ ; +} # end of add_exprs + + +## +## register a subroutine reference that will be called whenever +## ptkdb sets up it's windows +## +sub register_user_window_init { + push @{$DB::window->{'user_window_init_list'}}, @_ ; +} # end of register_user_window_init + +## +## register a subroutine reference that will be called whenever +## ptkdb enters from code +## +sub register_user_DB_entry { + push @{$DB::window->{'user_window_DB_entry_list'}}, @_ ; +} # end of register_user_DB_entry + +sub get_notebook_widget { + return $DB::window->{'notebook'} ; +} # end of get_notebook_widget + + +# +# Run files provided by the user +# +sub do_user_init_files { + use vars qw($dbg_window) ; + local $dbg_window = shift ; + + eval { + do "$Config{'installprivlib'}/Devel/ptkdbrc" ; + } if -e "$Config{'installprivlib'}/Devel/ptkdbrc" ; + + if( $@ ) { + print "System init file $Config{'installprivlib'}/ptkdbrc failed: $@\n" ; + } + + eval { + do "$ENV{'HOME'}/.ptkdbrc" ; + } if exists $ENV{'HOME'} && -e "$ENV{'HOME'}/.ptkdbrc" ; + + if( $@ ) { + print "User init file $ENV{'HOME'}/.ptkdbrc failed: $@\n" ; + } + + eval { + do ".ptkdbrc" ; + } if -e ".ptkdbrc" ; + + if( $@ ) { + print "User init file .ptkdbrc failed: $@\n" ; + } + + &set_stop_on_warning() ; +} + +# +# Constructor for our Devel::ptkdb +# +sub new { + my($type) = @_ ; + my($self) = {} ; + + bless $self, $type ; + + # Current position of the executing program + + $self->{DisableOnLeave} = [] ; # List o' Widgets to disable when leaving the debugger + + $self->{current_file} = "" ; + $self->{current_line} = -1 ; # initial value indicating we haven't set our line/tag + $self->{window_pos_offset} = 10 ; # when we enter how far from the top of the text are we positioned down + $self->{search_start} = "0.0" ; + $self->{fwdOrBack} = 1 ; + $self->{BookMarksPath} = $ENV{'PTKDB_BOOKMARKS_PATH'} || "$ENV{'HOME'}/.ptkdb_bookmarks" || '.ptkdb_bookmarks' ; + + $self->{'expr_list'} = [] ; # list of expressions to eval in our window fields: {'expr'} The expr itself {'depth'} expansion depth + + + $self->{'brkPtCnt'} = 0 ; + $self->{'brkPtSlots'} = [] ; # open slots for adding breakpoints to the table + + $self->{'main_window'} = undef ; + + $self->{'user_window_init_list'} = [] ; + $self->{'user_window_DB_entry_list'} = [] ; + + $self->setup_main_window() ; + + return $self ; + +} # end of new + +sub setup_main_window { + my($self) = @_ ; + + # Main Window + + $self->{main_window} = MainWindow->new() ; + $self->{main_window}->geometry($ENV{'PTKDB_GEOMETRY'} || "800x600") ; + + $self->setup_options() ; # must be done after MainWindow and before other frames are setup + + $self->{main_window}->bind('<Control-c>', \&DB::dbint_handler) ; + + # + # Bind our 'quit' routine to a close command from the window manager (Alt-F4) + # + $self->{main_window}->protocol('WM_DELETE_WINDOW', sub { $self->close_ptkdb_window() ; } ) ; + + # Menu bar + + $self->setup_menu_bar() ; + + # + # setup Frames + # + # Setup our Code, Data, and breakpoints + + $self->setup_frames() ; + +} + + +# +# This supports the File -> Open menu item +# We create a new window and list all of the files +# that are contained in the program. We also +# pick up all of the perlTk files that are supporting +# the debugger. +# +sub DoOpen { + my $self = shift ; + my ($topLevel, $listBox, $frame, $selectedFile, @fList) ; + + # + # subroutine we call when we've selected a file + # + + my $chooseSub = sub { $selectedFile = $listBox->get('active') ; + print "attempting to open $selectedFile\n" ; + $DB::window->set_file($selectedFile, 0) ; + destroy $topLevel ; + } ; + + # + # Take the list the files and resort it. + # we put all of the local files first, and + # then list all of the system libraries. + # + @fList = sort { + # sort comparison function block + my $fa = substr($a, 0, 1) ; + my $fb = substr($b, 0, 1) ; + + return $a cmp $b if ($fa eq '/') && ($fb eq '/') ; + + return -1 if ($fb eq '/') && ($fa ne '/') ; + return 1 if ($fa eq '/' ) && ($fb ne '/') ; + + return $a cmp $b ; + + } grep s/^_<//, keys %main:: ; + + # + # Create a list box with all of our files + # to select from + # + $topLevel = $self->{main_window}->Toplevel(-title => "File Select", -overanchor => 'cursor') ; + + $listBox = $topLevel->Scrolled('Listbox', + @Devel::ptkdb::scrollbar_cfg, + @Devel::ptkdb::expression_text_font, + 'width' => 30)->pack(side => 'top', fill => 'both', -expand => 1) ; + + + # Bind a double click on the mouse button to the same action + # as pressing the Okay button + + $listBox->bind('<Double-Button-1>' => $chooseSub) ; + + $listBox->insert('end', @fList) ; + + $topLevel->Button( text => "Okay", -command => $chooseSub, @Devel::ptkdb::button_font, + )->pack(side => 'left', fill => 'both', -expand => 1) ; + + $topLevel->Button( text => "Cancel", @Devel::ptkdb::button_font, + -command => sub { destroy $topLevel ; } )->pack(side => 'left', fill => 'both', -expand => 1) ; +} # end of DoOpen + +sub do_tabs { + my($tabs_str) ; + my($w, $result, $tabs_cfg) ; + require Tk::Dialog ; + + $w = $DB::window->{'main_window'}->DialogBox(-title => "Tabs", -buttons => [qw/Okay Cancel/]) ; + + $tabs_cfg = $DB::window->{'text'}->cget(-tabs) ; + + $tabs_str = join " ", @$tabs_cfg if $tabs_cfg ; + + $w->add('Label', -text => 'Tabs:')->pack(-side => 'left') ; + + $w->add('Entry', -textvariable => \$tabs_str)->pack(-side => 'left')->selectionRange(0,'end') ; + + $result = $w->Show() ; + + return unless $result eq 'Okay' ; + + $DB::window->{'text'}->configure(-tabs => [ split /\s/, $tabs_str ]) ; +} + +sub close_ptkdb_window { + my($self) = @_ ; + + $DB::window->{'event'} = 'run' ; + $self->{current_file} = "" ; # force a file reset + $self->{'main_window'}->destroy ; + $self->{'main_window'} = undef ; +} + +sub setup_menu_bar { + my ($self) = @_ ; + my $mw = $self->{main_window} ; + my ($mb, $items) ; + + # + # We have menu items/features that are not available if the Data::DataDumper module + # isn't present. For any feature that requires it we add this option list. + # + my @dataDumperEnableOpt = ( state => 'disabled' ) unless $Devel::ptkdb::DataDumperAvailable ; + + + $self->{menu_bar} = $mw->Frame(-relief => 'raised', -borderwidth => '1')->pack(side => 'top', -fill => 'x') ; + + $mb = $self->{menu_bar} ; + + # file menu in menu bar + + $items = [ [ 'command' => 'About...', -command => sub { $self->DoAbout() ; } ], + "-", + + [ 'command' => 'Open', -accelerator => 'Alt+O', + -underline => 0, + -command => sub { $self->DoOpen() ; } ], + + [ 'command' => 'Save Config...', + -underline => 0, + -command => \&DB::SaveState, + @dataDumperEnableOpt ], + + [ 'command' => 'Restore Config...', + -underline => 0, + -command => \&DB::RestoreState, + @dataDumperEnableOpt ], + + [ 'command' => 'Goto Line...', + -underline => 0, + -accelerator => 'Alt-g', + -command => \&DB::RestoreState, + @dataDumperEnableOpt ] , + + [ 'command' => 'Find Text...', + -accelerator => 'Ctrl-f', + -underline => 0, + -command => sub { $self->FindText() ; } ], + + [ 'command' => "Tabs...", -command => \&do_tabs ], + + "-", + + [ 'command' => 'Close Window and Run', -accelerator => 'Alt+W', + -underline => 6, -command => sub { $self->close_ptkdb_window ; } ], + + [ 'command' => 'Quit...', -accelerator => 'Alt+Q', + -underline => 0, + -command => sub { exit } ] + ] ; + + + $mw->bind('<Alt-g>' => sub { $self->GotoLine() ; }) ; + $mw->bind('<Control-f>' => sub { $self->FindText() ; }) ; + $mw->bind('<Control-r>' => \&Devel::ptkdb::DoRestart) ; + $mw->bind('<Alt-q>' => sub { $self->{'event'} = 'quit' } ) ; + $mw->bind('<Alt-w>' => sub { $self->close_ptkdb_window ; }) ; + + $self->{file_menu_button} = $mb->Menubutton(text => 'File', + underline => 0, + -menuitems => $items + )->pack(side =>, 'left', + anchor => 'nw', + 'padx' => 2) ; + + # Control Menu + + my $runSub = sub { $DB::step_over_depth = -1 ; $self->{'event'} = 'run' } ; + + my $runToSub = sub { $DB::window->{'event'} = 'run' if $DB::window->SetBreakPoint(1) ; } ; + + my $stepOverSub = sub { &DB::SetStepOverBreakPoint(0) ; + $DB::single = 1 ; + $DB::window->{'event'} = 'step' ; + } ; + + + my $stepInSub = sub { + $DB::step_over_depth = -1 ; + $DB::single = 1 ; + $DB::window->{'event'} = 'step' ; } ; + + + my $returnSub = sub { + &DB::SetStepOverBreakPoint(-1) ; + $self->{'event'} = 'run' ; + } ; + + + $items = [ [ 'command' => 'Run', -accelerator => 'Alt+r', underline => 0, -command => $runSub ], + [ 'command' => 'Run To Here', -accelerator => 'Alt+t', -underline => 5, -command => $runToSub ], + '-', + [ 'command' => 'Set Breakpoint', -underline => 4, -command => sub { $self->SetBreakPoint ; }, -accelerator => 'Ctrl-b' ], + [ 'command' => 'Clear Breakpoint', -command => sub { $self->UnsetBreakPoint } ], + [ 'command' => 'Clear All Breakpoints', -underline => 6, -command => sub { + $DB::window->removeAllBreakpoints($DB::window->{current_file}) ; + &DB::clearalldblines() ; + } ], + '-', + [ 'command' => 'Step Over', -accelerator => 'Alt+N', -underline => 0, -command => $stepOverSub ], + [ 'command' => 'Step In', -accelerator => 'Alt+S', -underline => 5, -command => $stepInSub ], + [ 'command' => 'Return', -accelerator => 'Alt+U', -underline => 3, -command => $returnSub ], + '-', + [ 'command' => 'Restart...', -accelerator => 'Ctrl-r', -underline => 0, -command => \&Devel::ptkdb::DoRestart ], + '-', + [ 'checkbutton' => 'Stop On Warning', -variable => \$DB::ptkdb::stop_on_warning, -command => \&set_stop_on_warning ] + + + ] ; # end of control menu items + + + $self->{control_menu_button} = $mb->Menubutton(text => 'Control', + -underline => 0, + -menuitems => $items, + )->pack(side =>, 'left', + 'padx' => 2) ; + + + $mw->bind('<Alt-r>' => $runSub) ; + $mw->bind('<Alt-t>', $runToSub) ; + $mw->bind('<Control-b>', sub { $self->SetBreakPoint ; }) ; + + for( @Devel::ptkdb::step_over_keys ) { + $mw->bind($_ => $stepOverSub ); + } + + for( @Devel::ptkdb::step_in_keys ) { + $mw->bind($_ => $stepInSub ); + } + + for( @Devel::ptkdb::return_keys ) { + $mw->bind($_ => $returnSub ); + } + + # Data Menu + + $items = [ [ 'command' => 'Enter Expression', -accelerator => 'Alt+E', -command => sub { $self->EnterExpr() } ], + [ 'command' => 'Delete Expression', -accelerator => 'Ctrl+D', -command => sub { $self->deleteExpr() } ], + [ 'command' => 'Delete All Expressions', -command => sub { + $self->deleteAllExprs() ; + $self->{'expr_list'} = [] ; # clears list by dropping ref to it, replacing it with a new one + } ], + '-', + [ 'command' => 'Expression Eval Window...', -accelerator => 'F8', -command => sub { $self->setupEvalWindow() ; } ], + [ 'checkbutton' => "Use DataDumper for Eval Window?", -variable => \$Devel::ptkdb::useDataDumperForEval, @dataDumperEnableOpt ] + ] ; + + + $self->{data_menu_button} = $mb->Menubutton(text => 'Data', -menuitems => $items, + underline => 0, + )->pack(side => 'left', + 'padx' => 2) ; + + $mw->bind('<Alt-e>' => sub { $self->EnterExpr() } ) ; + $mw->bind('<Control-d>' => sub { $self->deleteExpr() } ); + $mw->bind('<F8>', sub { $self->setupEvalWindow() ; }) ; + # + # Stack menu + # + $self->{stack_menu} = $mb->Menubutton(text => 'Stack', + underline => 2, + )->pack(side => 'left', + 'padx' => 2) ; + + # + # Bookmarks menu + # + $self->{bookmarks_menu} = $mb->Menubutton('text' => 'Bookmarks', + underline => 0, + @dataDumperEnableOpt + )->pack(-side => 'left', + 'padx' => 2) ; + $self->setup_bookmarks_menu() ; + + # + # Windows Menu + # + my($bsub) = sub { $self->{'text'}->focus() } ; + my($csub) = sub { $self->{'quick_entry'}->focus() } ; + my($dsub) = sub { $self->{'entry'}->focus() } ; + + $items = [ [ 'command' => 'Code Pane', -accelerator => 'Alt+0', -command => $bsub ], + [ 'command' => 'Quick Entry', -accelerator => 'F9', -command => $csub ], + [ 'command' => 'Expr Entry', -accelerator => 'F11', -command => $dsub ] + ] ; + + $mb->Menubutton('text' => 'Windows', -menuitems => $items + )->pack(-side => 'left', + -padx => 2) ; + + $mw->bind('<Alt-0>', $bsub) ; + $mw->bind('<F9>', $csub) ; + $mw->bind('<F11>', $dsub) ; + + # + # Bar for some popular controls + # + + $self->{button_bar} = $mw->Frame()->pack(side => 'top') ; + + $self->{stepin_button} = $self->{button_bar}->Button(-text, => "Step In", @Devel::ptkdb::button_font, + -command => $stepInSub) ; + $self->{stepin_button}->pack(-side => 'left') ; + + $self->{stepover_button} = $self->{button_bar}->Button(-text, => "Step Over", @Devel::ptkdb::button_font, + -command => $stepOverSub) ; + $self->{stepover_button}->pack(-side => 'left') ; + + $self->{return_button} = $self->{button_bar}->Button(-text, => "Return", @Devel::ptkdb::button_font, + -command => $returnSub) ; + $self->{return_button}->pack(-side => 'left') ; + + $self->{run_button} = $self->{button_bar}->Button(-background => 'green', -text, => "Run", @Devel::ptkdb::button_font, + -command => $runSub) ; + $self->{run_button}->pack(-side => 'left') ; + + $self->{run_to_button} = $self->{button_bar}->Button(-text, => "Run To", @Devel::ptkdb::button_font, + -command => $runToSub) ; + $self->{run_to_button}->pack(-side => 'left') ; + + $self->{breakpt_button} = $self->{button_bar}->Button(-text, => "Break", @Devel::ptkdb::button_font, + -command => sub { $self->SetBreakPoint ; } ) ; + $self->{breakpt_button}->pack(-side => 'left') ; + + push @{$self->{DisableOnLeave}}, @$self{'stepin_button', 'stepover_button', 'return_button', 'run_button', 'run_to_button', 'breakpt_button'} ; + +} # end of setup_menu_bar + +sub edit_bookmarks { + my ($self) = @_ ; + + my ($top) = $self->{main_window}->Toplevel(-title => "Edit Bookmarks") ; + + my $list = $top->Scrolled('Listbox', -selectmode => 'multiple')->pack(-side => 'top', -fill => 'both', -expand => 1) ; + + my $deleteSub = sub { + my $cnt = 0 ; + for( $list->curselection ) { + $list->delete($_ - $cnt++) ; + } + } ; + + my $okaySub = sub { + $self->{'bookmarks'} = [ $list->get(0, 'end') ] ; # replace the bookmarks + } ; + + my $frm = $top->Frame()->pack(-side => 'top', -fill => 'x', -expand => 1 ) ; + + my $deleteBtn = $frm->Button(-text => 'Delete', -command => $deleteSub)->pack(-side => 'left', -fill => 'x', -expand => 1 ) ; + my $cancelBtn = $frm->Button(-text => 'Cancel', -command => sub { destroy $top ; })->pack(-side =>'left', -fill => 'x', -expand => 1 ) ; + my $dismissBtn = $frm->Button(-text => 'Okay', -command => $okaySub)->pack(-side => 'left', -fill => 'x', -expand => 1 ) ; + + $list->insert('end', @{$self->{'bookmarks'}}) ; + +} # end of edit_bookmarks + +sub setup_bookmarks_menu { + my ($self) = @_ ; + + # + # "Add bookmark" item + # + my $bkMarkSub = sub { $self->add_bookmark() ; } ; + + $self->{'bookmarks_menu'}->command(-label => "Add Bookmark", + -accelerator => 'Alt+k', + -command => $bkMarkSub + ) ; + + $self->{'main_window'}->bind('<Alt-k>', $bkMarkSub) ; + + $self->{'bookmarks_menu'}->command(-label => "Edit Bookmarks", + -command => sub { $self->edit_bookmarks() } ) ; + + $self->{'bookmarks_menu'}->separator() ; + + # + # Check to see if there is a bookmarks file + # + return unless -e $self->{BookMarksPath} && -r $self->{BookMarksPath} ; + + use vars qw($ptkdb_bookmarks) ; + local($ptkdb_bookmarks) ; # ref to hash of bookmark entries + + do $self->{BookMarksPath} ; # eval the file + + $self->add_bookmark_items(@$ptkdb_bookmarks) ; + +} # end of setup_bookmarks_menu + +# +# $item = "$fname:$lineno" +# +sub add_bookmark_items { + my($self, @items) = @_ ; + my($menu) = ( $self->{'bookmarks_menu'} ) ; + + $self->{'bookmarks_changed'} = 1 ; + + for( @items ) { + my $item = $_ ; + $menu->command( -label => $_, + -command => sub { $self->bookmark_cmd($item) }) ; + push @{$self->{'bookmarks'}}, $item ; + } +} # end of add_bookmark_item + +# +# Invoked from the "Add Bookmark" command +# +sub add_bookmark { + my($self) = @_ ; + + my $line = $self->get_lineno() ; + my $fname = $self->{'current_file'} ; + $self->add_bookmark_items("$fname:$line") ; + +} # end of add_bookmark + +# +# Command executed when someone selects +# a bookmark +# +sub bookmark_cmd { + my ($self, $item) = @_ ; + + $item =~ /(.*):([0-9]+)$/ ; + + $self->set_file($1,$2) ; + +} # end of bookmark_cmd + +sub save_bookmarks { + my($self, $pathName) = @_ ; + + return unless $Devel::ptkdb::DataDumperAvailable ; # we can't save without the data dumper + local(*F) ; + + eval { + open F, ">$pathName" || die "open failed" ; + my $d = Data::Dumper->new([ $self->{'bookmarks'} ], + [ 'ptkdb_bookmarks' ]) ; + + $d->Indent(2) ; # make it more editable for people + + my $str ; + if( $d->can('Dumpxs') ) { + $str = $d->Dumpxs() ; + } + else { + $str = $d->Dump() ; + } + + print F $str || die "outputing bookmarks failed" ; + close(F) ; + } ; + + if( $@ ) { + $self->DoAlert("Couldn't save bookmarks file $@") ; + return ; + } + +} # end of save_bookmarks + +# +# This is our callback from a double click in our +# HList. A click in an expanded item will delete +# the children beneath it, and the next time it +# updates, it will only update that entry to that +# depth. If an item is 'unexpanded' such as +# a hash or a list, it will expand it one more +# level. How much further an item is expanded is +# controled by package variable $Devel::ptkdb::add_expr_depth +# +sub expr_expand { + my ($path) = @_ ; + my $hl = $DB::window->{'data_list'} ; + my ($parent, $root, $index, @children, $depth) ; + + $parent = $path ; + $root = $path ; + $depth = 0 ; + + for( $root = $path ; defined $parent && $parent ne "" ; $parent = $hl->infoParent($root) ) { + $root = $parent ; + $depth += 1 ; + } #end of root search + + # + # Determine the index of the root of our expression + # + $index = 0 ; + for( @{$DB::window->{'expr_list'}} ) { + last if $_->{'expr'} eq $root ; + $index += 1 ; + } + + # + # if we have children we're going to delete them + # + + @children = $hl->infoChildren($path) ; + + if( scalar @children > 0 ) { + + $hl->deleteOffsprings($path) ; + + $DB::window->{'expr_list'}->[$index]->{'depth'} = $depth - 1 ; # adjust our depth + } + else { + # + # Delete the existing tree and insert a new one + # + $hl->deleteEntry($root) ; + $hl->add($root, -at => $index) ; + $DB::window->{'expr_list'}->[$index]->{'depth'} += $Devel::ptkdb::add_expr_depth ; + # + # Force an update on our expressions + # + $DB::window->{'event'} = 'update' ; + } +} # end of expr_expand + +sub line_number_from_coord { + my($txtWidget, $coord) = @_ ; + my($index) ; + + $index = $txtWidget->index($coord) ; + + # index is in the format of lineno.column + + $index =~ /([0-9]*)\.([0-9]*)/o ; + + # + # return a list of (col, line). Why + # backwards? + # + + return ($2 ,$1) ; + +} # end of line_number_from_coord + +# +# It may seem as if $txtWidget and $self are +# erroneously reversed, but this is a result +# of the calling syntax of the text-bind callback. +# +sub set_breakpoint_tag { + my($txtWidget, $self, $coord, $value) = @_ ; + my($idx) ; + + $idx = line_number_from_coord($txtWidget, $coord) ; + + $self->insertBreakpoint($self->{'current_file'}, $idx, $value) ; + +} # end of set_breakpoint_tag + +sub clear_breakpoint_tag { + my($txtWidget, $self, $coord) = @_ ; + my($idx) ; + + $idx = line_number_from_coord($txtWidget, $coord) ; + + $self->removeBreakpoint($self->{'current_file'}, $idx) ; + +} # end of clear_breakpoint_tag + +sub change_breakpoint_tag { + my($txtWidget, $self, $coord, $value) = @_ ; + my($idx, $brkPt, @tagSet) ; + + $idx = line_number_from_coord($txtWidget, $coord) ; + + # + # Change the value of the breakpoint + # + @tagSet = ( "$idx.0", "$idx.$Devel::ptkdb::linenumber_length" ) ; + + $brkPt = &DB::getdbline($self->{'current_file'}, $idx + $self->{'line_offset'}) ; + return unless $brkPt ; + + # + # Check the breakpoint tag + # + + if ( $txtWidget ) { + $txtWidget->tagRemove('breaksetLine', @tagSet ) ; + $txtWidget->tagRemove('breakdisabledLine', @tagSet ) ; + } + + $brkPt->{'value'} = $value ; + + if ( $txtWidget ) { + if ( $brkPt->{'value'} ) { + $txtWidget->tagAdd('breaksetLine', @tagSet ) ; + } + else { + $txtWidget->tagAdd('breakdisabledLine', @tagSet ) ; + } + } + +} # end of change_breakpoint_tag + +# +# God Forbid anyone comment something complex and tightly optimized. +# +# We can get a list of the subroutines from the interpreter +# by querrying the *DB::sub typeglob: keys %DB::sub +# +# The list appears broken down by module: +# +# main::BEGIN +# main::mySub +# main::otherSub +# Tk::Adjuster::Mapped +# Tk::Adjuster::Packed +# Tk::Button::BEGIN +# Tk::Button::Enter +# +# We would like to break this list down into a heirarchy. +# +# main Tk +# | | | | +# BEGIN mySub OtherSub | | +# Adjuster Button +# | | | | +# Mapped Packed BEGIN Enter +# +# +# We translate this list into a heirarchy of hashes(say three times fast). +# We take each entry and split it into elements. Each element is a leaf in the tree. +# We traverse the tree with the inner for loop. +# With each branch we check to see if it already exists or +# we create it. When we reach the last element, this becomes our entry. +# + +# +# An incoming list is potentially 'large' so we +# pass in the ref to it instead. +# +# New entries can be inserted by providing a $topH +# hash ref to an existing tree. +# +sub tree_split { + my ($listRef, $separator, $topH) = @_ ; + my ($h, $list_elem) ; + + $topH = {} unless $topH ; + + foreach $list_elem ( @$listRef ) { + $h = $topH ; + for( split /$separator/o, $list_elem ) { # Tk::Adjuster::Mapped -> ( Tk Adjuster Mapped ) + $h->{$_} or $h->{$_} = {} ; # either we have an entry for this OR we create one + $h = $h->{$_} ; + } + @$h{'name', 'path'} = ($_, $list_elem) ; # the last leaf is our entry + } # end of tree_split loop + + return $topH ; + +} # end of tree_split + +# +# callback executed when someone double clicks +# an entry in the 'Subs' Tk::Notebook page. +# +sub sub_list_cmd { + my ($self, $path) = @_ ; + my ($h) ; + my $sub_list = $self->{'sub_list'} ; + + if ( $sub_list->info('children', $path) ) { + # + # Delete the children + # + $sub_list->deleteOffsprings($path) ; + return ; + } + + # + # split the path up into elements + # end descend through the tree. + # + $h = $Devel::ptkdb::subs_tree ; + for ( split /\./o, $path ) { + $h = $h->{$_} ; # next level down + } + + # + # if we don't have a 'name' entry we + # still have levels to decend through. + # + if ( !exists $h->{'name'} ) { + # + # Add the next level paths + # + for ( sort keys %$h ) { + + if ( exists $h->{$_}->{'path'} ) { + $sub_list->add($path . '.' . $_, -text => $h->{$_}->{'path'}) ; + } + else { + $sub_list->add($path . '.' . $_, -text => $_) ; + } + } + return ; + } + + $DB::sub{$h->{'path'}} =~ /(.*):([0-9]+)-[0-9]+$/o ; # file name will be in $1, line number will be in $2 */ + + $self->set_file($1, $2) ; + +} # end of sub_list_cmd + +sub fill_subs_page { + my($self) = @_ ; + + $self->{'sub_list'}->delete('all') ; # clear existing entries + + my @list = keys %DB::sub ; + + $Devel::ptkdb::subs_tree = tree_split(\@list, "::") ; + + # setup to level of list + + for ( sort keys %$Devel::ptkdb::subs_tree ) { + $self->{'sub_list'}->add($_, -text => $_) ; + } # end of top level loop +} + +sub setup_subs_page { + my($self) = @_ ; + + $self->{'subs_page_activated'} = 1 ; + + $self->{'sub_list'} = $self->{'subs_page'}->Scrolled('HList', -command => sub { $self->sub_list_cmd(@_) ; } ) ; + + $self->fill_subs_page() ; + + $self->{'sub_list'}->pack(side => 'left', fill => 'both', expand => 1 + ) ; + + $self->{'subs_list_cnt'} = scalar keys %DB::sub ; + + +} # end of setup_subs_page + +sub setup_search_panel { + my ($self, $parent, @packArgs) = @_ ; + my ($frm, $srchBtn, $regexBtn, $entry) ; + + $frm = $parent->Frame() ; + + $frm->Button(-text => 'Goto', -command => sub { $self->DoGoto($entry) })->pack(side => 'left') ; + $srchBtn = $frm->Button(-text => 'Search', -command => sub { $self->FindSearch($entry, $srchBtn, 0) ; } + )->pack(side => 'left' ) ; + + $regexBtn = $frm->Button(-text => 'Regex', + -command => sub { $self->FindSearch($entry, $regexBtn, 1) ; } + )->pack(side => 'left', + ) ; + + + $entry = $frm->Entry(width => 50)->pack(side => 'left', fill => 'both', expand => 1) ; + + $frm->pack(@packArgs) ; + +} # end of setup search_panel + +sub setup_breakpts_page { + my ($self) = @_ ; + require Tk::Table ; + + $self->{'breakpts_page'} = $self->{'notebook'}->add("brkptspage", -label => "BrkPts") ; + + $self->{'breakpts_table'} = $self->{'breakpts_page'}->Table(-columns => 1, -scrollbars => 'se')-> + pack(side => 'top', fill => 'both', expand => 1 + ) ; + + $self->{'breakpts_table_data'} = { } ; # controls addressed by "fname:lineno" + +} # end of setup_breakpts_page + +sub setup_frames { + my ($self) = @_ ; + my $mw = $self->{'main_window'} ; + my ($txt, $place_holder, $frm) ; + require Tk::ROText ; + require Tk::NoteBook ; + require Tk::HList ; + require Tk::Balloon ; + require Tk::Adjuster ; + + # get the side that we want to put the code pane on + + my($codeSide) = $ENV{'PTKDB_CODE_SIDE'} || $mw->optionGet("codeside", "") || 'left' ; + + + + $mw->update ; # force geometry manager to map main_window + $frm = $mw->Frame(-width => $mw->reqwidth()) ; # frame for our code pane and search controls + + $self->setup_search_panel($frm, side => 'top', fill => 'x') ; + + # + # Text window for the code of our currently viewed file + # + $self->{'text'} = $frm->Scrolled('ROText', + -wrap => "none", + @Devel::ptkdb::scrollbar_cfg, + @Devel::ptkdb::code_text_font + ) ; + + + $txt = $self->{'text'} ; + for( $txt->children ) { + next unless (ref $_) =~ /ROText$/ ; + $self->{'text'} = $_ ; + last ; + } + + $frm->packPropagate(0) ; + $txt->packPropagate(0) ; + + $frm->packAdjust(side => $codeSide, fill => 'both', expand => 1) ; + $txt->pack(side => 'left', fill => 'both', expand => 1) ; + + # $txt->form(-top => [ $self->{'menu_bar'} ], -left => '%0', -right => '%50') ; + # $frm->form(-top => [ $self->{'menu_bar'} ], -left => '%50', -right => '%100') ; + + $self->configure_text() ; + + # + # Notebook + # + + $self->{'notebook'} = $mw->NoteBook() ; + $self->{'notebook'}->packPropagate(0) ; + $self->{'notebook'}->pack(side => $codeSide, fill => 'both', -expand => 1) ; + + # + # an hlist for the data entries + # + $self->{'data_page'} = $self->{'notebook'}->add("datapage", -label => "Exprs") ; + + # + # frame, entry and label for quick expressions + # + my $frame = $self->{'data_page'}->Frame()->pack(side => 'top', fill => 'x') ; + + my $label = $frame->Label('text' => "Quick Expr:")->pack(side => 'left') ; + + $self->{'quick_entry'} = $frame->Entry()->pack(side => 'left', fill => 'x', -expand => 1) ; + + $self->{'quick_entry'}->bind('<Return>', sub { $self->QuickExpr() ; } ) ; + + + # + # Entry widget for expressions and breakpoints + # + $frame = $self->{'data_page'}->Frame()->pack(side => 'top', fill => 'x') ; + + $label = $frame->Label('text' => "Enter Expr:")->pack(side => 'left') ; + + $self->{'entry'} = $frame->Entry()->pack(side => 'left', fill => 'x', -expand => 1) ; + + $self->{'entry'}->bind('<Return>', sub { $self->EnterExpr() }) ; + + # + # Hlist for data expressions + # + + + $self->{data_list} = $self->{'data_page'}->Scrolled('HList', + @Devel::ptkdb::scrollbar_cfg, + separator => $Devel::ptkdb::pathSep, + @Devel::ptkdb::expression_text_font, + -command => \&Devel::ptkdb::expr_expand, + -selectmode => 'multiple' + ) ; + + $self->{data_list}->pack(side => 'top', fill => 'both', expand => 1 + ) ; + + + $self->{'subs_page_activated'} = 0 ; + $self->{'subs_page'} = $self->{'notebook'}->add("subspage", -label => "Subs", -createcmd => sub { $self->setup_subs_page }) ; + + $self->setup_breakpts_page() ; + +} # end of setup_frames + + + +sub configure_text { + my($self) = @_ ; + my($txt, $mw) = ($self->{'text'}, $self->{'main_window'}) ; + my($place_holder) ; + + $self->{'expr_balloon'} = $txt->Balloon(); + $self->{'balloon_expr'} = ' ' ; # initial expression + + # If Data::Dumper is available setup a dumper for the balloon + + if ( $Devel::ptkdb::DataDumperAvailable ) { + $self->{'balloon_dumper'} = new Data::Dumper([$place_holder]) ; + $self->{'balloon_dumper'}->Terse(1) ; + $self->{'balloon_dumper'}->Indent($Devel::ptkdb::eval_dump_indent) ; + + $self->{'quick_dumper'} = new Data::Dumper([$place_holder]) ; + $self->{'quick_dumper'}->Terse(1) ; + $self->{'quick_dumper'}->Indent(0) ; + } + + $self->{'expr_ballon_msg'} = ' ' ; + + $self->{'expr_balloon'}->attach($txt, -initwait => 300, + -msg => \$self->{'expr_ballon_msg'}, + -balloonposition => 'mouse', + -postcommand => \&Devel::ptkdb::balloon_post, + -motioncommand => \&Devel::ptkdb::balloon_motion ) ; + + # tags for the text + + my @stopTagConfig = ( -foreground => 'white', -background => $mw->optionGet("stopcolor", "background") || $ENV{'PTKDB_STOP_TAG_COLOR'} || 'blue' ) ; + + my $stopFnt = $mw->optionGet("stopfont", "background") || $ENV{'PTKDB_STOP_TAG_FONT'} ; + push @stopTagConfig, ( -font => $stopFnt ) if $stopFnt ; # user may not have specified a font, if not, stay with the default + + $txt->tagConfigure('stoppt', @stopTagConfig) ; + $txt->tagConfigure('search_tag', "-background" => $mw->optionGet("searchtagcolor", "background") || "green") ; + + $txt->tagConfigure("breakableLine", -overstrike => 0) ; + $txt->tagConfigure("nonbreakableLine", -overstrike => 1) ; + $txt->tagConfigure("breaksetLine", -background => $mw->optionGet("breaktagcolor", "background") || $ENV{'PTKDB_BRKPT_COLOR'} || 'red') ; + $txt->tagConfigure("breakdisabledLine", -background => $mw->optionGet("disabledbreaktagcolor", "background") || $ENV{'PTKDB_DISABLEDBRKPT_COLOR'} || 'green') ; + + $txt->tagBind("breakableLine", '<Button-1>', [ \&Devel::ptkdb::set_breakpoint_tag, $self, Ev('@'), 1 ] ) ; + $txt->tagBind("breakableLine", '<Shift-Button-1>', [ \&Devel::ptkdb::set_breakpoint_tag, $self, Ev('@'), 0 ] ) ; + + $txt->tagBind("breaksetLine", '<Button-1>', [ \&Devel::ptkdb::clear_breakpoint_tag, $self, Ev('@') ] ) ; + $txt->tagBind("breaksetLine", '<Shift-Button-1>', [ \&Devel::ptkdb::change_breakpoint_tag, $self, Ev('@'), 0 ] ) ; + + $txt->tagBind("breakdisabledLine", '<Button-1>', [ \&Devel::ptkdb::clear_breakpoint_tag, $self, Ev('@') ] ) ; + $txt->tagBind("breakdisabledLine", '<Shift-Button-1>', [ \&Devel::ptkdb::change_breakpoint_tag, $self, Ev('@'), 1 ] ) ; + +} # end of configure_text + + +sub setup_options { + my ($self) = @_ ; + my $mw = $self->{main_window} ; + + return unless $mw->can('appname') ; + + $mw->appname("ptkdb") ; + $mw->optionAdd("stopcolor" => 'cyan', 60 ) ; + $mw->optionAdd("stopfont" => 'fixed', 60 ) ; + $mw->optionAdd("breaktag" => 'red', 60 ) ; + $mw->optionAdd("searchtagcolor" => 'green') ; + + $mw->optionClear ; # necessary to reload xresources + +} # end of setup_options + +sub DoAlert { + my($self, $msg, $title) = @_ ; + my($dlg) ; + my $okaySub = sub { + destroy $dlg ; + } ; + + $dlg = $self->{main_window}->Toplevel(-title => $title || "Alert", -overanchor => 'cursor') ; + + $dlg->Label( 'text' => $msg )->pack( side => 'top' ) ; + + $dlg->Button( 'text' => "Okay", -command => $okaySub )->pack( side => 'top' )->focus ; + $dlg->bind('<Return>', $okaySub) ; + +} # end of DoAlert + +sub simplePromptBox { + my ($self, $title, $defaultText, $okaySub, $cancelSub) = @_ ; + my ($top, $entry, $okayBtn) ; + + $top = $self->{main_window}->Toplevel(-title => $title, -overanchor => 'cursor' ) ; + + $Devel::ptkdb::promptString = $defaultText ; + + $entry = $top->Entry('-textvariable' => 'Devel::ptkdb::promptString')->pack('side' => 'top', fill => 'both', -expand => 1) ; + + + $okayBtn = $top->Button( text => "Okay", @Devel::ptkdb::button_font, -command => sub { &$okaySub() ; $top->destroy ;} + )->pack(side => 'left', fill => 'both', -expand => 1) ; + + $top->Button( text => "Cancel", -command => sub { &$cancelSub() if $cancelSub ; $top->destroy() }, @Devel::ptkdb::button_font, + )->pack(side => 'left', fill => 'both', -expand => 1) ; + + $entry->icursor('end') ; + + $entry->selectionRange(0, 'end') if $entry->can('selectionRange') ; # some win32 Tk installations can't do this + + $entry->focus() ; + + return $top ; + +} # end of simplePromptBox + +sub get_entry_text { + my($self) = @_ ; + + return $self->{entry}->get() ; # get the text in the entry +} # end of get_entry_text + + +# +# Clear any text that is in the entry field. If there +# was any text in that field return it. If there +# was no text then return any selection that may be active. +# +sub clear_entry_text { + my($self) = @_ ; + my $str = $self->{'entry'}->get() ; + $self->{'entry'}->delete(0, 'end') ; + + # + # No String + # Empty String + # Or a string that is only whitespace + # + if( !$str || $str eq "" || $str =~ /^\s+$/ ) { + # + # If there is no string or the string is just white text + # Get the text in the selction( if any) + # + if( $self->{'text'}->tagRanges('sel') ) { # check to see if 'sel' tag exists (return undef value) + $str = $self->{'text'}->get("sel.first", "sel.last") ; # get the text between the 'first' and 'last' point of the sel (selection) tag + } + # If still no text, bring the focus to the entry + elsif( !$str || $str eq "" || $str =~ /^\s+$/ ) { + $self->{'entry'}->focus() ; + $str = "" ; + } + } + # + # Erase existing text + # + return $str ; +} # end of clear_entry_text + +sub brkPtCheckbutton { + my ($self, $fname, $idx, $brkPt) = @_ ; + my ($widg) ; + + change_breakpoint_tag($self->{'text'}, $self, "$idx.0", $brkPt->{'value'}) if $fname eq $self->{'current_file'} ; + +} # end of brkPtCheckbutton + +# +# insert a breakpoint control into our breakpoint list. +# returns a handle to the control +# +# Expression, if defined, is to be evaluated at the breakpoint +# and execution stopped if it is non-zero/defined. +# +# If action is defined && True then it will be evalled +# before continuing. +# +sub insertBreakpoint { + my ($self, $fname, @brks) = @_ ; + my ($btn, $cnt, $item) ; + + my($offset) ; + local(*dbline) = $main::{'_<' . $fname} ; + + $offset = $dbline[1] =~ /use\s+.*Devel::_?ptkdb/ ? 1 : 0 ; + + while( @brks ) { + my($index, $value, $expression) = splice @brks, 0, 3 ; # take args 3 at a time + + my $brkPt = {} ; + my $txt = &DB::getdbtextline($fname, $index) ; + @$brkPt{'type', 'line', 'expr', 'value', 'fname', 'text'} = + ('user', $index, $expression, $value, $fname, "$txt") ; + + &DB::setdbline($fname, $index + $offset, $brkPt) ; + $self->add_brkpt_to_brkpt_page($brkPt) ; + + next unless $fname eq $self->{'current_file'} ; + + $self->{'text'}->tagRemove("breakableLine", "$index.0", "$index.$Devel::ptkdb::linenumber_length") ; + $self->{'text'}->tagAdd($value ? "breaksetLine" : "breakdisabledLine", "$index.0", "$index.$Devel::ptkdb::linenumber_length") ; + } # end of loop +} # end of insertBreakpoint + +sub add_brkpt_to_brkpt_page { + my($self, $brkPt) = @_ ; + my($btn, $fname, $index, $frm, $upperFrame, $lowerFrame) ; + my ($row, $btnName, $width) ; + # + # Add the breakpoint to the breakpoints page + # + ($fname, $index) = @$brkPt{'fname', 'line'} ; + return if exists $self->{'breakpts_table_data'}->{"$fname:$index"} ; + $self->{'brkPtCnt'} += 1 ; + + $btnName = $fname ; + $btnName =~ s/.*\/([^\/]*)$/$1/o ; + + # take the last leaf of the pathname + + $frm = $self->{'breakpts_table'}->Frame(-relief => 'raised') ; + $upperFrame = $frm->Frame()->pack('side' => 'top', '-fill' => 'x', 'expand' => 1) ; + + + $btn = $upperFrame->Checkbutton(-text => "$btnName:$index", + -variable => \$brkPt->{'value'}, # CAUTION value tracking + -command => sub { $self->brkPtCheckbutton($fname, $index, $brkPt) }) ; + + $btn->pack(side => 'left') ; + + $btn = $upperFrame->Button(-text => "Delete", -command => sub { $self->removeBreakpoint($fname, $index) ; } ) ; + $btn->pack('side' => 'left', -fill => 'x', -expand => 1) ; + + $btn = $upperFrame->Button(-text => "Goto", -command => sub { $self->set_file($fname, $index) ; } ) ; + $btn->pack('side' => 'left', -fill => 'x', -expand => 1) ; + + $lowerFrame = $frm->Frame()->pack('side' => 'top', '-fill' => 'x', 'expand' => 1) ; + + $lowerFrame->Label(-text => "Cond:")->pack('side' => 'left') ; + + $btn = $lowerFrame->Entry(-textvariable => \$brkPt->{'expr'}) ; + $btn->pack('side' => 'left', fill => 'x', -expand => 1) ; + + $frm->pack(side => 'top', fill => 'x', -expand => 1) ; + + $row = pop @{$self->{'brkPtSlots'}} or $row = $self->{'brkPtCnt'} ; + + $self->{'breakpts_table'}->put($row, 1, $frm) ; + + $self->{'breakpts_table_data'}->{"$fname:$index"}->{'frm'} = $frm ; + $self->{'breakpts_table_data'}->{"$fname:$index"}->{'row'} = $row ; + + $self->{'main_window'}->update ; + + $width = $frm->width ; + + if ( $width > $self->{'breakpts_table'}->width ) { + $self->{'notebook'}->configure(-width => $width) ; + } + +} # end of add_brkpt_to_brkpt_page + +sub remove_brkpt_from_brkpt_page { + my($self, $fname, $idx) = @_ ; + my($table) ; + + $table = $self->{'breakpts_table'} ; + + # Delete the breakpoint control in the breakpoints window + + $table->put($self->{'breakpts_table_data'}->{"$fname:$idx"}->{'row'}, 1) ; # delete? + + # + # Add this now empty slot to the list of ones we have open + # + + push @{$self->{'brkPtSlots'}}, $self->{'breakpts_table_data'}->{"$fname:$idx"}->{'row'} ; + + $self->{'brkPtSlots'} = [ sort { $b <=> $a } @{$self->{'brkPtSlots'}} ] ; + + delete $self->{'breakpts_table_data'}->{"$fname:$idx"} ; + + $self->{'brkPtCnt'} -= 1 ; + +} # end of remove_brkpt_from_brkpt_page + + +# +# Supporting the "Run To Here..." command +# +sub insertTempBreakpoint { + my ($self, $fname, $index) = @_ ; + my($offset) ; + local(*dbline) = $main::{'_<' . $fname} ; + + $offset = $dbline[1] =~ /use\s+.*Devel::_?ptkdb/ ? 1 : 0 ; + + return if( &DB::getdbline($fname, $index + $offset) ) ; # we already have a breakpoint here + + &DB::setdbline($fname, $index + $offset, {'type' => 'temp', 'line' => $index, 'value' => 1 } ) ; + +} # end of insertTempBreakpoint + +sub reinsertBreakpoints { + my ($self, $fname) = @_ ; + my ($brkPt) ; + + foreach $brkPt ( &DB::getbreakpoints($fname) ) { + # + # Our breakpoints are indexed by line + # therefore we can have 'gaps' where there + # lines, but not breaks set for them. + # + next unless defined $brkPt ; + + $self->insertBreakpoint($fname, @$brkPt{'line', 'value', 'expr'}) if( $brkPt->{'type'} eq 'user' ) ; + $self->insertTempBreakpoint($fname, $brkPt->{line}) if( $brkPt->{'type'} eq 'temp' ) ; + } # end of reinsert loop + +} # end of reinsertBreakpoints + +sub removeBreakpointTags { + my ($self, @brkPts) = @_ ; + my($idx, $brkPt) ; + + foreach $brkPt (@brkPts) { + + $idx = $brkPt->{'line'} ; + + if ( $brkPt->{'value'} ) { + $self->{'text'}->tagRemove("breaksetLine", "$idx.0", "$idx.$Devel::ptkdb::linenumber_length") ; + } + else { + $self->{'text'}->tagRemove("breakdisabledLine", "$idx.0", "$idx.$Devel::ptkdb::linenumber_length") ; + } + + $self->{'text'}->tagAdd("breakableLine", "$idx.0", "$idx.$Devel::ptkdb::linenumber_length") ; + } +} # end of removeBreakpointTags + +# +# Remove a breakpoint from the current window +# +sub removeBreakpoint { + my ($self, $fname, @idx) = @_ ; + my ($idx, $chkIdx, $i, $j, $info) ; + my($offset) ; + local(*dbline) = $main::{'_<' . $fname} ; + + $offset = $dbline[1] =~ /use\s+.*Devel::_?ptkdb/ ? 1 : 0 ; + + foreach $idx (@idx) { # end of removal loop + next unless defined $idx ; + my $brkPt = &DB::getdbline($fname, $idx + $offset) ; + next unless $brkPt ; # if we do not have an entry + &DB::cleardbline($fname, $idx + $offset) ; + + $self->remove_brkpt_from_brkpt_page($fname, $idx) ; + + next unless $brkPt->{fname} eq $self->{'current_file'} ; # if this isn't our current file there will be no controls + + # Delete the ext associated with the breakpoint expression (if any) + + $self->removeBreakpointTags($brkPt) ; + } # end of remove loop + + return ; +} # end of removeBreakpoint + +sub removeAllBreakpoints { + my ($self, $fname) = @_ ; + + $self->removeBreakpoint($fname, &DB::getdblineindexes($fname)) ; + +} # end of removeAllBreakpoints + +# +# Delete expressions prior to an update +# +sub deleteAllExprs { + my ($self) = @_ ; + $self->{'data_list'}->delete('all') ; +} # end of deleteAllExprs + +sub EnterExpr { + my ($self) = @_ ; + my $str = $self->clear_entry_text() ; + if( $str && $str ne "" && $str !~ /^\s+$/ ) { # if there is an expression and it's more than white space + $self->{'expr'} = $str ; + $self->{'event'} = 'expr' ; + } +} # end of EnterExpr + +# +# +# +sub QuickExpr { + my ($self) = @_ ; + + my $str = $self->{'quick_entry'}->get() ; + + if( $str && $str ne "" && $str !~ /^\s+$/ ) { # if there is an expression and it's more than white space + $self->{'qexpr'} = $str ; + $self->{'event'} = 'qexpr' ; + } +} # end of QuickExpr + +sub deleteExpr { + my ($self) = @_ ; + my ($entry, $i, @indexes) ; + my @sList = $self->{'data_list'}->info('select') ; + + # + # if we're deleteing a top level expression + # we have to take it out of the list of expressions + # + + foreach $entry ( @sList ) { + next if ($entry =~ /\//) ; # goto next expression if we're not a top level ( expr/entry) + $i = 0 ; + grep { push @indexes, $i if ($_->{'expr'} eq $entry) ; $i++ ; } @{$self->{'expr_list'}} ; + } # end of check loop + + # now take out our list of indexes ; + + for( 0..$#indexes ) { + splice @{$self->{'expr_list'}}, $indexes[$_] - $_, 1 ; + } + + for( @sList ) { + $self->{'data_list'}->delete('entry', $_) ; + } +} # end of deleteExpr + +sub fixExprPath { + my(@pathList) = @_ ; + + for (@pathList) { + s/$Devel::ptkdb::pathSep/$Devel::ptkdb::pathSepReplacement/go ; + } # end of path list + + return $pathList[0] unless wantarray ; + return @pathList ; + +} # end of fixExprPath + +## +## Inserts an expression($theRef) into an HList Widget($dl). If the expression +## is an array, blessed array, hash, or blessed hash(typical object), then this +## routine is called recursively, adding the members to the next level of heirarchy, +## prefixing array members with a [idx] and the hash members with the key name. +## This continues until the entire expression is decomposed to it's atomic constituents. +## Protection is given(with $reusedRefs) to ensure that 'circular' references within +## arrays or hashes(i.e. where a member of a array or hash contains a reference to a +## parent element within the heirarchy. +## +# +# Returns 1 if sucessfully added 0 if not +# +sub insertExpr { + my($self, $reusedRefs, $dl, $theRef, $name, $depth, $dirPath) = @_ ; + my($label, $type, $result, $selfCnt, @circRefs) ; + local($^W) = 0 ; # spare us uncessary warnings about comparing strings with == + + # + # Add data new data entries to the bottom + # + $dirPath = "" unless defined $dirPath ; + + $label = "" ; + $selfCnt = 0 ; + + while( ref $theRef eq 'SCALAR' ) { + $theRef = $$theRef ; + } + REF_CHECK: for( ; ; ) { + push @circRefs, $theRef ; + $type = ref $theRef ; + last unless ($type eq "REF") ; + $theRef = $$theRef ; # dref again + + $label .= "\\" ; # append a + if( grep $_ == $theRef, @circRefs ) { + $label .= "(circular)" ; + last ; + } + } + + if( !$type || $type eq "" || $type eq "GLOB" || $type eq "CODE") { + eval { + if( !defined $theRef ) { + $dl->add($dirPath . $name, -text => "$name = $label" . "undef") ; + } + else { + $dl->add($dirPath . $name, -text => "$name = $label$theRef") ; + } + } ; + $self->DoAlert($@), return 0 if $@ ; + return 1 ; + } + + if( $type eq 'ARRAY' or "$theRef" =~ /ARRAY/ ) { + my ($r, $idx) ; + $idx = 0 ; + eval { + $dl->add($dirPath . $name, -text => "$name = $theRef") ; + } ; + if( $@ ) { + $self->DoAlert($@) ; + return 0 ; + } + $result = 1 ; + foreach $r ( @{$theRef} ) { + + if( grep $_ == $r, @$reusedRefs ) { # check to make sure that we're not doing a single level self reference + eval { + $dl->add($dirPath . fixExprPath($name) . $Devel::ptkdb::pathSep . "__ptkdb_self_path" . $selfCnt++, -text => "[$idx] = $r REUSED ADDR") ; + } ; + $self->DoAlert($@) if( $@ ) ; + next ; + } + + $^W = 0 ; + + push @$reusedRefs, $r ; + $result = $self->insertExpr($reusedRefs, $dl, $r, "[$idx]", $depth-1, $dirPath . fixExprPath($name) . $Devel::ptkdb::pathSep) unless $depth == 0 ; + pop @$reusedRefs ; + + return 0 unless $result ; + $idx += 1 ; + } + return 1 ; + } # end of array case + + if( "$theRef" !~ /HASH\050\060x[0-9a-f]*\051/o ) { + eval { + $dl->add($dirPath . fixExprPath($name), -text => "$name = $theRef") ; + } ; + if( $@ ) { + $self->DoAlert($@) ; + return 0 ; + } + return 1 ; + } +# +# Anything else at this point is +# either a 'HASH' or an object +# of some kind. +# + my($r, @theKeys, $idx) ; + $idx = 0 ; + @theKeys = sort keys %{$theRef} ; + $dl->add($dirPath . $name, -text => "$name = $theRef") ; + $result = 1 ; + + foreach $r ( @$theRef{@theKeys} ) { # slice out the values with the sorted list + + if( grep $_ == $r, @$reusedRefs ) { # check to make sure that we're not doing a single level self reference + eval { + $dl->add($dirPath . fixExprPath($name) . $Devel::ptkdb::pathSep . "__ptkdb_self_path" . $selfCnt++, -text => "$theKeys[$idx++] = $r REUSED ADDR") ; + } ; + print "bad path $@\n" if( $@ ) ; + next ; + } + + $^W = 0 ; + + push @$reusedRefs, $r ; + + $result = $self->insertExpr($reusedRefs, # recursion protection + $dl, # data list widget + $r, # reference whose value is displayed + $theKeys[$idx], # name + $depth-1, # remaining expansion depth + $dirPath . $name . $Devel::ptkdb::pathSep # path to add to + ) unless $depth == 0 ; + + pop @$reusedRefs ; + + return 0 unless $result ; + $idx += 1 ; + } # end of ref add loop + + return 1 ; +} # end of insertExpr + +# +# We're setting the line where we are stopped. +# Create a tag for this and set it as bold. +# +sub set_line { + my ($self, $lineno) = @_ ; + my $text = $self->{'text'} ; + + return if( $lineno <= 0 ) ; + + if( $self->{current_line} > 0 ) { + $text->tagRemove('stoppt', "$self->{current_line}.0 linestart", "$self->{current_line}.0 lineend") ; + } + $self->{current_line} = $lineno - $self->{'line_offset'} ; + $text->tagAdd('stoppt', "$self->{current_line}.0 linestart", "$self->{current_line}.0 lineend") ; + + $self->{'text'}->see("$self->{current_line}.0 linestart") ; +} # end of set_line + +# +# Set the file that is in the code window. +# +# $fname the 'new' file to view +# $line the line number we're at +# $brkPts any breakpoints that may have been set in this file +# + +use Carp ; + +sub set_file { + my ($self, $fname, $line) = @_ ; + my ($lineStr, $offset, $text, $i, @text) ; + my (@breakableTagList, @nonBreakableTagList) ; + + return unless $fname ; # we're getting an undef here on 'Restart...' + + local(*dbline) = $main::{'_<' . $fname}; + + # + # with the #! /usr/bin/perl -d:ptkdb at the header of the file + # we've found that with various combinations of other options the + # files haven't come in at the right offsets + # + $offset = 0 ; + $offset = 1 if $dbline[1] =~ /use\s+.*Devel::_?ptkdb/ ; + $self->{'line_offset'} = $offset ; + + $text = $self->{'text'} ; + + if( $fname eq $self->{current_file} ) { + $self->set_line($line) ; + return ; + } ; + + $fname =~ s/^\-// ; # Tk does not like leadiing '-'s + $self->{main_window}->configure('-title' => $fname) ; + + # Erase any existing text + + $text->delete('0.0','end') ; + + my $len = $Devel::ptkdb::linenumber_length ; + + # + # This is the tightest loop we have in the ptkdb code. + # It is here where performance is the most critical. + # The map block formats perl code for display. Since + # the file could be potentially large, we will try + # to make this loop as thin as possible. + # + # NOTE: For a new perl individual this may appear as + # if it was intentionally obfuscated. This is not + # not the case. The following code is the result + # of an intensive effort to optimize this code. + # Prior versions of this code were quite easier + # to read, but took 3 times longer. + # + + $lineStr = " " x 200 ; # pre-allocate space for $lineStr + $i = 1 ; + + local($^W) = 0 ; # spares us useless warnings under -w when checking $dbline[$_] != 0 + # + # The 'map' call will build list of 'string', 'tag' pairs + # that will become arguments to the 'insert' call. Passing + # the text to insert "all at once" rather than one insert->('end', 'string', 'tag') + # call at time provides a MASSIVE savings in execution time. + # + + $text->insert('end', map { + + # + # build collections of tags representing + # the line numbers for breakable and + # non-breakable lines. We apply these + # tags after we've built the text + # + + ($_ != 0 && push @breakableTagList, "$i.0", "$i.$len") || push @nonBreakableTagList, "$i.0", "$i.$len" ; + + $lineStr = sprintf($Devel::ptkdb::linenumber_format, $i++) . $_ ; # line number + text of the line + $lineStr .= "\n" unless /\n$/o ; # append a \n if there isn't one already + + ($lineStr, 'code') ; # return value for block, a string,tag pair for text insert + + } @dbline[$offset+1 .. $#dbline] ) ; + + + # + # Apply the tags that we've collected + # NOTE: it was attempted to incorporate these + # operations into the 'map' block above, but that + # actually degraded performance. + # + $text->tagAdd("breakableLine", @breakableTagList) if @breakableTagList ; # apply tag to line numbers where the lines are breakable + $text->tagAdd("nonbreakableLine", @nonBreakableTagList) if @nonBreakableTagList ; # apply tag to line numbers where the lines are not breakable. + + # + # Reinsert breakpoints (if info provided) + # + + $self->set_line($line) ; + $self->{current_file} = $fname ; + return $self->reinsertBreakpoints($fname) ; + + } # end of set_file + +# +# Get the current line that the insert cursor is in +# + sub get_lineno { + my ($self) = @_ ; + my ($info) ; + + $info = $self->{'text'}->index('insert') ; # get the location for the insertion point + $info =~ s/\..*$/\.0/ ; + + return int $info ; + } # end of get_lineno + +sub DoGoto { + my ($self, $entry) = @_ ; + + my $txt = $entry->get() ; + + $txt =~ s/(\d*).*/$1/ ; # take the first blob of digits + if( $txt eq "" ) { + print "invalid text range\n" ; + return if $txt eq "" ; + } + + $self->{'text'}->see("$txt.0") ; + + $entry->selectionRange(0, 'end') if $entry->can('selectionRange') + + } # end of DoGoto + +sub GotoLine { + my ($self) = @_ ; + my ($topLevel) ; + + if( $self->{goto_window} ) { + $self->{goto_window}->raise() ; + $self->{goto_text}->focus() ; + return ; + } + + # + # Construct a dialog that has an + # entry field, okay and cancel buttons + # + my $okaySub = sub { $self->DoGoto($self->{'goto_text'}) } ; + + $topLevel = $self->{main_window}->Toplevel(-title => "Goto Line?", -overanchor => 'cursor') ; + + $self->{goto_text} = $topLevel->Entry()->pack(side => 'top', fill => 'both', -expand => 1) ; + + $self->{goto_text}->bind('<Return>', $okaySub) ; # make a CR do the same thing as pressing an okay + + $self->{goto_text}->focus() ; + + # Bind a double click on the mouse button to the same action + # as pressing the Okay button + + $topLevel->Button( text => "Okay", -command => $okaySub, @Devel::ptkdb::button_font, + )->pack(side => 'left', fill => 'both', -expand => 1) ; + + # + # Subroutone called when the 'Dismiss' + # button is pushed. + # + my $dismissSub = sub { + delete $self->{goto_text} ; + destroy {$self->{goto_window}} ; + delete $self->{goto_window} ; # remove the entry from our hash so we won't + } ; + + $topLevel->Button( text => "Dismiss", @Devel::ptkdb::button_font, + -command => $dismissSub )->pack(side => 'left', fill => 'both', -expand => 1) ; + + $topLevel->protocol('WM_DELETE_WINDOW', sub { destroy $topLevel ; } ) ; + + $self->{goto_window} = $topLevel ; + +} # end of GotoLine + + +# +# Subroutine called when the 'okay' button is pressed +# +sub FindSearch { + my ($self, $entry, $btn, $regExp) = @_ ; + my (@switches, $result) ; + my $txt = $entry->get() ; + + return if $txt eq "" ; + + push @switches, "-forward" if $self->{fwdOrBack} eq "forward" ; + push @switches, "-backward" if $self->{fwdOrBack} eq "backward" ; + + if( $regExp ) { + push @switches, "-regexp" ; + } + else { + push @switches, "-nocase" ; # if we're not doing regex we may as well do caseless search + } + + $result = $self->{'text'}->search(@switches, $txt, $self->{search_start}) ; + + # untag the previously found text + + $self->{'text'}->tagRemove('search_tag', @{$self->{search_tag}}) if defined $self->{search_tag} ; + + if( !$result || $result eq "" ) { + # No Text was found + $btn->flash() ; + $btn->bell() ; + + delete $self->{search_tag} ; + $self->{'search_start'} = "0.0" ; + } + else { # text found + $self->{'text'}->see($result) ; + # set the insertion of the text as well + $self->{'text'}->markSet('insert' => $result) ; + my $len = length $txt ; + + if( $self->{fwdOrBack} ) { + $self->{search_start} = "$result +$len chars" ; + $self->{search_tag} = [ $result, $self->{search_start} ] ; + } + else { + # backwards search + $self->{search_start} = "$result -$len chars" ; + $self->{search_tag} = [ $result, "$result +$len chars" ] ; + } + + # tag the newly found text + + $self->{'text'}->tagAdd('search_tag', @{$self->{search_tag}}) ; + } # end of text found + + $entry->selectionRange(0, 'end') if $entry->can('selectionRange') ; + +} # end of FindSearch + + +# +# Support for the Find Text... Menu command +# +sub FindText { + my ($self) = @_ ; + my ($top, $entry, $rad1, $rad2, $chk, $regExp, $frm, $okayBtn) ; + + # + # if we already have the Find Text Window + # open don't bother openning another, bring + # the existing one to the front. + # + if( $self->{find_window} ) { + $self->{find_window}->raise() ; + $self->{find_text}->focus() ; + return ; + } + + $self->{search_start} = $self->{'text'}->index('insert') if( $self->{search_start} eq "" ) ; + + # + # Subroutine called when the 'Dismiss' button + # is pushed. + # + my $dismissSub = sub { + $self->{'text'}->tagRemove('search_tag', @{$self->{search_tag}}) if defined $self->{search_tag} ; + $self->{search_start} = "" ; + destroy {$self->{find_window}} ; + delete $self->{search_tag} ; + delete $self->{find_window} ; + } ; + + # + # Construct a dialog that has an entry field, forward, backward, regex option, okay and cancel buttons + # + $top = $self->{main_window}->Toplevel(-title => "Find Text?") ; + + $self->{find_text} = $top->Entry()->pack('side' => 'top', fill => 'both', -expand => 1) ; + + + $frm = $top->Frame()->pack('side' => 'top', fill => 'both', -expand => 1) ; + + $self->{fwdOrBack} = 'forward' ; + $rad1 = $frm->Radiobutton('text' => "Forward", 'value' => 1, 'variable' => \$self->{fwdOrBack}) ; + $rad1->pack(side => 'left', fill => 'both', -expand => 1) ; + $rad2 = $frm->Radiobutton('text' => "Backward", 'value' => 0, 'variable' => \$self->{fwdOrBack}) ; + $rad2->pack(side => 'left', fill => 'both', -expand => 1) ; + + $regExp = 0 ; + $chk = $frm->Checkbutton('text' => "RegExp", 'variable' => \$regExp) ; + $chk->pack(side => 'left', fill => 'both', -expand => 1) ; + + # Okay and cancel buttons + + # Bind a double click on the mouse button to the same action + # as pressing the Okay button + + $okayBtn = $top->Button( text => "Okay", -command => sub { $self->FindSearch($self->{find_text}, $okayBtn, $regExp) ; }, + @Devel::ptkdb::button_font, + )->pack(side => 'left', fill => 'both', -expand => 1) ; + + $self->{find_text}->bind('<Return>', sub { $self->FindSearch($self->{find_text}, $okayBtn, $regExp) ; }) ; + + $top->Button( text => "Dismiss", @Devel::ptkdb::button_font, + -command => $dismissSub)->pack(side => 'left', fill => 'both', -expand => 1) ; + + $top->protocol('WM_DELETE_WINDOW', $dismissSub) ; + + $self->{find_text}->focus() ; + + $self->{find_window} = $top ; + +} # end of FindText + +sub main_loop { + my ($self) = @_ ; + my ($evt, $str, $result) ; + my $i = 0; + SWITCH: for ($self->{'event'} = 'null' ; ; $self->{'event'} = undef ) { + + Tk::DoOneEvent(0); + next unless $self->{'event'} ; + + $evt = $self->{'event'} ; + $evt =~ /step/o && do { last SWITCH ; } ; + $evt =~ /null/o && do { next SWITCH ; } ; + $evt =~ /run/o && do { last SWITCH ; } ; + $evt =~ /quit/o && do { $self->{main_window}->destroy if $self->{main_window} ; + $self->{main_window} = undef if defined $self->{main_window} ; exit ; } ; + $evt =~ /expr/o && do { return $evt ; } ; # adds an expression to our expression window + $evt =~ /qexpr/o && do { return $evt ; } ; # does a 'quick' expression + $evt =~ /update/o && do { return $evt ; } ; # forces an update on our expression window + $evt =~ /reeval/o && do { return $evt ; } ; # updated the open expression eval window + $evt =~ /balloon_eval/ && do { return $evt } ; + } # end of switch block + return $evt ; +} # end of main_loop + +# +# $subStackRef A reference to the current subroutine stack +# + +sub goto_sub_from_stack { + my ($self, $f, $lineno) = @_ ; + $self->set_file($f, $lineno) ; +} # end of goto_sub_from_stack ; + +sub refresh_stack_menu { + my ($self) = @_ ; + my ($str, $name, $i, $sub_offset, $subStack) ; + + # + # CAUTION: In the effort to 'rationalize' the code + # are moving some of this function down from DB::DB + # to here. $sub_offset represents how far 'down' + # we are from DB::DB. The $DB::subroutine_depth is + # tracked in such a way that while we are 'in' the debugger + # it will not be incremented, and thus represents the stack depth + # of the target program. + # + $sub_offset = 1 ; + $subStack = [] ; + + # clear existing entries + + for( $i = 0 ; $i <= $DB::subroutine_depth ; $i++ ) { + my ($package, $filename, $line, $subName) = caller $i+$sub_offset ; + last if !$subName ; + push @$subStack, { 'name' => $subName, 'pck' => $package, 'filename' => $filename, 'line' => $line } ; + } + + $self->{stack_menu}->menu->delete(0, 'last') ; # delete existing menu items + + for( $i = 0 ; $subStack->[$i] ; $i++ ) { + + $str = defined $subStack->[$i+1] ? "$subStack->[$i+1]->{name}" : "MAIN" ; + + my ($f, $line) = ($subStack->[$i]->{filename}, $subStack->[$i]->{line}) ; # make copies of the values for use in 'sub' + $self->{stack_menu}->command(-label => $str, -command => sub { $self->goto_sub_from_stack($f, $line) ; } ) ; + } +} # end of refresh_stack_menu + +no strict ; + +sub get_state { + my ($self, $fname) = @_ ; + my ($val) ; + local($files, $expr_list, $eval_saved_text, $main_win_geometry) ; + + do "$fname" ; + + if( $@ ) { + $self->DoAlert($@) ; + return ( undef ) x 4 ; # return a list of 4 undefined values + } + + return ($files, $expr_list, $eval_saved_text, $main_win_geometry) ; +} # end of get_state + +use strict ; + +sub restoreStateFile { + my ($self, $fname) = @_ ; + local(*F) ; + my ($saveCurFile, $s, @n, $n) ; + + if (!(-e $fname && -r $fname)) { + $self->DoAlert("$fname does not exist") ; + return ; + } + + my ($files, $expr_list, $eval_saved_text, $main_win_geometry) = $self->get_state($fname) ; + my ($f, $brks) ; + + return unless defined $files || defined $expr_list ; + + &DB::restore_breakpoints_from_save($files) ; + + # + # This should force the breakpoints to be restored + # + $saveCurFile = $self->{current_file} ; + + @$self{ 'current_file', 'expr_list', 'eval_saved_text' } = + ( "" , $expr_list, $eval_saved_text) ; + + $self->set_file($saveCurFile, $self->{current_line}) ; + + $self->{'event'} = 'update' ; + + if ( $main_win_geometry && $self->{'main_window'} ) { + # restore the height and width of the window + $self->{main_window}->geometry( $main_win_geometry ) ; + } +} # end of retstoreState + +sub updateEvalWindow { + my ($self, @result) = @_ ; + my ($leng, $str, $d) ; + + $leng = 0 ; + for( @result ) { + if( !$Devel::ptkdb::DataDumperAvailable || !$Devel::ptkdb::useDataDumperForEval ) { + $str = "$_\n" ; + } + else { + $d = Data::Dumper->new([ $_ ]) ; + $d->Indent($Devel::ptkdb::eval_dump_indent) ; + $d->Terse(1) ; + if( Data::Dumper->can('Dumpxs') ) { + $str = $d->Dumpxs( $_ ) ; + } + else { + $str = $d->Dump( $_ ) ; + } + } + $leng += length $str ; + $self->{eval_results}->insert('end', $str) ; + } +} # end of updateEvalWindow + +sub setupEvalWindow { + my($self) = @_ ; + my($top, $dismissSub) ; + my $f ; + $self->{eval_window}->focus(), return if exists $self->{eval_window} ; # already running this window? + + $top = $self->{main_window}->Toplevel(-title => "Evaluate Expressions...") ; + $self->{eval_window} = $top ; + $self->{eval_text} = $top->Scrolled('TextUndo', + @Devel::ptkdb::scrollbar_cfg, + @Devel::ptkdb::eval_text_font, + width => 50, + height => 10, + -wrap => "none", + )->packAdjust('side' => 'top', 'fill' => 'both', -expand => 1) ; + + $self->{eval_text}->insert('end', $self->{eval_saved_text}) if exists $self->{eval_saved_text} && defined $self->{eval_saved_text} ; + + $top->Label(-text, "Results:")->pack('side' => 'top', 'fill' => 'both', -expand => 'n') ; + + $self->{eval_results} = $top->Scrolled('Text', + @Devel::ptkdb::scrollbar_cfg, + width => 50, + height => 10, + -wrap => "none", + @Devel::ptkdb::eval_text_font + )->pack('side' => 'top', 'fill' => 'both', -expand => 1) ; + + my $btn = $top->Button(-text => 'Eval...', -command => sub { $DB::window->{event} = 'reeval' ; } + )->pack('side' => 'left', 'fill' => 'x', -expand => 1) ; + + $dismissSub = sub { + $self->{eval_saved_text} = $self->{eval_text}->get('0.0', 'end') ; + $self->{eval_window}->destroy ; + delete $self->{eval_window} ; + } ; + + $top->protocol('WM_DELETE_WINDOW', $dismissSub ) ; + + $top->Button(-text => 'Clear Eval', -command => sub { $self->{eval_text}->delete('0.0', 'end') } + )->pack('side' => 'left', 'fill' => 'x', -expand => 1) ; + + $top->Button(-text => 'Clear Results', -command => sub { $self->{eval_results}->delete('0.0', 'end') } + )->pack('side' => 'left', 'fill' => 'x', -expand => 1) ; + + $top->Button(-text => 'Dismiss', -command => $dismissSub)->pack('side' => 'left', 'fill' => 'x', -expand => 1) ; + +} # end of setupEvalWindow ; + +sub filterBreakPts { + my ($breakPtsListRef, $fname) = @_ ; + my $dbline = $main::{'_<' . $fname}; # breakable lines + local($^W) = 0 ; + # + # Go through the list of breaks and take out any that + # are no longer breakable + # + + for( @$breakPtsListRef ) { + next unless defined $_ ; + + next if $dbline->[$_->{'line'}] != 0 ; # still breakable + + $_ = undef ; + } +} # end of filterBreakPts + +sub DoAbout { + my $self = shift ; + my $str = "ptkdb $DB::VERSION\nCopyright 1998 by Andrew E. Page\nFeedback to aep\@world.std.com\n\n" ; + my $threadString = "" ; + + $threadString = "Threads Available" if $Config::Config{usethreads} ; + $threadString = " Thread Debugging Enabled" if $DB::usethreads ; + + $str .= <<"__STR__" ; + This program is free software; you can redistribute it and/or modify + it under the terms of either: + + a) the GNU General Public License as published by the Free + Software Foundation; either version 1, or (at your option) any + later version, or + + b) the "Artistic License" which comes with this Kit. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either + the GNU General Public License or the Artistic License for more details. + + OS $^O + Tk Version $Tk::VERSION + Perl Version $] +Data::Dumper Version $Data::Dumper::VERSION + $threadString +__STR__ + + $self->DoAlert($str, "About ptkdb") ; +} # end of DoAbout + +# +# return 1 if succesfully set, +# return 0 if otherwise +# +sub SetBreakPoint { + my ($self, $isTemp) = @_ ; + my $dbw = $DB::window ; + my $lineno = $dbw->get_lineno() ; + my $expr = $dbw->clear_entry_text() ; + local($^W) = 0 ; + + if( !&DB::checkdbline($DB::window->{current_file}, $lineno + $self->{'line_offset'}) ) { + $dbw->DoAlert("line $lineno in $DB::window->{current_file} is not breakable") ; + return 0 ; + } + + if( !$isTemp ) { + $dbw->insertBreakpoint($DB::window->{current_file}, $lineno, 1, $expr) ; + return 1 ; + } + else { + $dbw->insertTempBreakpoint($DB::window->{current_file}, $lineno) ; + return 1 ; + } + + return 0 ; +} # end of SetBreakPoint + +sub UnsetBreakPoint { + my ($self) = @_ ; + my $lineno = $self->get_lineno() ; + + $self->removeBreakpoint($DB::window->{current_file}, $lineno) ; +} # end of UnsetBreakPoint + +sub balloon_post { + my $self = $DB::window ; + my $txt = $DB::window->{'text'} ; + + return 0 if ($self->{'expr_ballon_msg'} eq "") || ($self->{'balloon_expr'} eq "") ; # don't post for an empty string + + return $self->{'balloon_coord'} ; +} + +sub balloon_motion { + my ($txt, $x, $y) = @_ ; + my ($offset_x, $offset_y) = ($x + 4, $y + 4) ; + my $self = $DB::window ; + my $txt2 = $self->{'text'} ; + my $data ; + + $self->{'balloon_coord'} = "$offset_x,$offset_y" ; + + $x -= $txt->rootx ; + $y -= $txt->rooty ; + # + # Post an event that will cause us to put up a popup + # + + if( $txt2->tagRanges('sel') ) { # check to see if 'sel' tag exists (return undef value) + $data = $txt2->get("sel.first", "sel.last") ; # get the text between the 'first' and 'last' point of the sel (selection) tag + } + else { + $data = $DB::window->retrieve_text_expr($x, $y) ; + } + + if( !$data ) { + $self->{'balloon_expr'} = "" ; + return 0 ; + } + + return 0 if ($data eq $self->{'balloon_expr'}) ; # nevermind if it's the same expression + + $self->{'event'} = 'balloon_eval' ; + $self->{'balloon_expr'} = $data ; + + return 1 ; # ballon will be canceled and a new one put up(maybe) +} # end of balloon_motion + +sub retrieve_text_expr { + my($self, $x, $y) = @_ ; + my $txt = $self->{'text'} ; + + my $coord = "\@$x,$y" ; + + my($idx, $col, $data, $offset) ; + + ($col, $idx) = line_number_from_coord($txt, $coord) ; + + $offset = $Devel::ptkdb::linenumber_length + 1 ; # line number text + 1 space + + return undef if $col < $offset ; # no posting + + $col -= $offset ; + + local(*dbline) = $main::{'_<' . $self->{current_file}} ; + + return undef if( !defined $dbline[$idx] || $dbline[$idx] == 0 ) ; # no executable text, no real variable(?) + + $data = $dbline[$idx] ; + + # if we're sitting over white space, leave + my $len = length $data ; + return unless $data && $col && $len > 0 ; + + return if substr($data, $col, 1) =~ /\s/ ; + + # walk backwards till we find some whitespace + + $col = $len if $len < $col ; + while( --$col >= 0 ) { + last if substr($data, $col, 1) =~ /[\s\$\@\%]/ ; + } + + substr($data, $col) =~ /^([\$\@\%][a-zA-Z0-9_]+)/ ; + + return $1 ; +} + +# +# after DB::eval get's us a result +# +sub code_motion_eval { + my ($self, @result) = @_ ; + my $str ; + + if( exists $self->{'balloon_dumper'} ) { + + my $d = $self->{'balloon_dumper'} ; + + $d->Reset() ; + $d->Values( [ $#result == 0 ? @result : \@result ] ) ; + + if( $d->can('Dumpxs') ) { + $str = $d->Dumpxs() ; + } + else { + $str = $d->Dump() ; + } + + chomp($str) ; + } + else { + $str = "@result" ; + } + + # + # Cut the string down to 1024 characters to keep from + # overloading the balloon window + # + + $self->{'expr_ballon_msg'} = "$self->{'balloon_expr'} = " . substr $str, 0, 1024 ; + } # end of code motion eval + +# +# Subroutine called when we enter DB::DB() +# In other words when the target script 'stops' +# in the Debugger +# +sub EnterActions { + my($self) = @_ ; + +# $self->{'main_window'}->Unbusy() ; + +} # end of EnterActions + +# +# Subroutine called when we return from DB::DB() +# When the target script resumes. +# +sub LeaveActions { + my($self) = @_ ; + + # $self->{'main_window'}->Busy() ; +} # end of LeaveActions + + +sub BEGIN { + $Devel::ptkdb::scriptName = $0 ; + @Devel::ptkdb::script_args = @ARGV ; # copy args + +} + +## +## Save the ptkdb state file and restart the debugger +## +sub DoRestart { + my($fname) ; + + $fname = $ENV{'TMP'} || $ENV{'TMPDIR'} || $ENV{'TMP_DIR'} || $ENV{'TEMP'} || $ENV{'HOME'} ; + $fname .= '/' if $fname ; + $fname = "" unless $fname ; + + $fname .= "ptkdb_restart_state$$" ; + + # print "saving temp state file $fname\n" ; + + &DB::save_state_file($fname) ; + + $ENV{'PTKDB_RESTART_STATE_FILE'} = $fname ; + + ## + ## build up the command to do the restart + ## + + $fname = "perl -w -d:ptkdb $Devel::ptkdb::scriptName @Devel::ptkdb::script_args" ; + + # print "$$ doing a restart with $fname\n" ; + + exec $fname ; + +} # end of DoRestart + +## +## Enables/Disables the feature where we stop +## if we've encountered a perl warning such as: +## "Use of uninitialized value at undef_warn.pl line N" +## + +sub stop_on_warning_cb { + &$DB::ptkdb::warn_sig_save() if $DB::ptkdb::warn_sig_save ; # call any previously registered warning + $DB::window->DoAlert(@_) ; + $DB::single = 1 ; # forces debugger to stop next time +} + +sub set_stop_on_warning { + + if( $DB::ptkdb::stop_on_warning ) { + + return if $DB::ptkdb::warn_sig_save == \&stop_on_warning_cb ; # prevents recursion + + $DB::ptkdb::warn_sig_save = $SIG{'__WARN__'} if $SIG{'__WARN__'} ; + $SIG{'__WARN__'} = \&stop_on_warning_cb ; + } + else { + ## + ## Restore any previous warning signal + ## + local($^W) = 0 ; + $SIG{'__WARN__'} = $DB::ptkdb::warn_sig_save ; + } +} # end of set_stop_on_warning + +1 ; # end of Devel::ptkdb + +package DB ; + +use vars '$VERSION', '$header' ; + +$VERSION = '1.108' ; +$header = "ptkdb.pm version $DB::VERSION"; +$DB::window->{current_file} = "" ; + +# +# Here's the clue... +# eval only seems to eval the context of +# the executing script while in the DB +# package. When we had updateExprs in the Devel::ptkdb +# package eval would turn up an undef result. +# + +sub updateExprs { + my ($package) = @_ ; + # + # Update expressions + # + $DB::window->deleteAllExprs() ; + my ($expr, @result); + + foreach $expr ( @{$DB::window->{'expr_list'}} ) { + next if length $expr == 0 ; + + @result = &DB::dbeval($package, $expr->{'expr'}) ; + + if( scalar @result == 1 ) { + $DB::window->insertExpr([ $result[0] ], $DB::window->{'data_list'}, $result[0], $expr->{'expr'}, $expr->{'depth'}) ; + } + else { + $DB::window->insertExpr([ \@result ], $DB::window->{'data_list'}, \@result, $expr->{'expr'}, $expr->{'depth'}) ; + } + } + +} # end of updateExprs + +no strict ; # turning strict off (shame shame) because we keep getting errrs for the local(*dbline) + +# +# returns true if line is breakable +# +use Carp ; +sub checkdbline($$) { + my ($fname, $lineno) = @_ ; + + return 0 unless $fname; # we're getting an undef here on 'Restart...' + + local(*dbline) = $main::{'_<' . $fname} ; + local($^W) = 0 ; # spares us warnings under -w + + my $flag = $dbline[$lineno] != 0 ; + + return $flag; + +} # end of checkdbline + +# +# sets a breakpoint 'through' a magic +# variable that perl is able to interpert +# +sub setdbline($$$) { + my ($fname, $lineno, $value) = @_ ; + local(*dbline) = $main::{'_<' . $fname}; + + $dbline{$lineno} = $value ; +} # end of setdbline + +sub getdbline($$) { + my ($fname, $lineno) = @_ ; + local(*dbline) = $main::{'_<' . $fname}; + return $dbline{$lineno} ; +} # end of getdbline + +sub getdbtextline { + my ($fname, $lineno) = @_ ; + local(*dbline) = $main::{'_<' . $fname}; + return $dbline[$lineno] ; +} # end of getdbline + + +sub cleardbline($$;&) { + my ($fname, $lineno, $clearsub) = @_ ; + local(*dbline) = $main::{'_<' . $fname}; + my $value ; # just in case we want it for something + + $value = $dbline{$lineno} ; + delete $dbline{$lineno} ; + &$clearsub($value) if $value && $clearsub ; + + return $value ; +} # end of cleardbline + +sub clearalldblines(;&) { + my ($clearsub) = @_ ; + my ($key, $value, $brkPt, $dbkey) ; + local(*dbline) ; + + while ( ($key, $value) = each %main:: ) { # key loop + next unless $key =~ /^_</ ; + *dbline = $value ; + + foreach $dbkey (keys %dbline) { + $brkPt = $dbline{$dbkey} ; + delete $dbline{$dbkey} ; + next unless $brkPt && $clearSub ; + &$clearsub($brkPt) ; # if specificed, call the sub routine to clear the breakpoint + } + + } # end of key loop + +} # end of clearalldblines + +sub getdblineindexes { + my ($fname) = @_ ; + local(*dbline) = $main::{'_<' . $fname} ; + return keys %dbline ; +} # end of getdblineindexes + +sub getbreakpoints { + my (@fnames) = @_ ; + my ($fname, @retList) ; + + foreach $fname (@fnames) { + next unless $main::{'_<' . $fname} ; + local(*dbline) = $main::{'_<' . $fname} ; + push @retList, values %dbline ; + } + return @retList ; +} # end of getbreakpoints + +# +# Construct a hash of the files +# that have breakpoints to save +# +sub breakpoints_to_save { + my ($file, @breaks, $brkPt, $svBrkPt, $list) ; + my ($brkList) ; + + $brkList = {} ; + + foreach $file ( keys %main:: ) { # file loop + next unless $file =~ /^_</ && exists $main::{$file} ; + local(*dbline) = $main::{$file} ; + + next unless @breaks = values %dbline ; + $list = [] ; + foreach $brkPt ( @breaks ) { + + $svBrkPt = { %$brkPt } ; # make a copy of it's data + + push @$list, $svBrkPt ; + + } # end of breakpoint loop + + $brkList->{$file} = $list ; + + } # end of file loop + + return $brkList ; + +} # end of breakpoints_to_save + +# +# When we restore breakpoints from a state file +# they've often 'moved' because the file +# has been editted. +# +# We search for the line starting with the original line number, +# then we walk it back 20 lines, then with line right after the +# orginal line number and walk forward 20 lines. +# +# NOTE: dbline is expected to be 'local' +# when called +# +sub fix_breakpoints { + my(@brkPts) = @_ ; + my($startLine, $endLine, $nLines, $brkPt) ; + my (@retList) ; + local($^W) = 0 ; + + $nLines = scalar @dbline ; + + foreach $brkPt (@brkPts) { + + $startLine = $brkPt->{'line'} > 20 ? $brkPt->{'line'} - 20 : 0 ; + $endLine = $brkPt->{'line'} < $nLines - 20 ? $brkPt->{'line'} + 20 : $nLines ; + + for( (reverse $startLine..$brkPt->{'line'}), $brkPt->{'line'} + 1 .. $endLine ) { + next unless $brkPt->{'text'} eq $dbline[$_] ; + $brkPt->{'line'} = $_ ; + push @retList, $brkPt ; + last ; + } + } # end of breakpoint list + + return @retList ; + +} # end of fix_breakpoints + +# +# Restore breakpoints saved above +# +sub restore_breakpoints_from_save { + my ($brkList) = @_ ; + my ($offset, $key, $list, $brkPt, @newList) ; + + while ( ($key, $list) = each %$brkList ) { # reinsert loop + next unless exists $main::{$key} ; + local(*dbline) = $main::{$key} ; + + $offset = 0 ; + $offset = 1 if $dbline[1] =~ /use\s+.*Devel::_?ptkdb/ ; + + @newList = fix_breakpoints(@$list) ; + + foreach $brkPt ( @newList ) { + if( !&DB::checkdbline($key, $brkPt->{'line'} + $offset) ) { + print "Breakpoint $key:$brkPt->{'line'} in config file is not breakable.\n" ; + next ; + } + $dbline{$brkPt->{'line'}} = { %$brkPt } ; # make a fresh copy + } + } # end of reinsert loop + +} # end of restore_breakpoints_from_save ; + +use strict ; + +sub dbint_handler { + my($sigName) = @_ ; + $DB::single = 1 ; + print "signalled\n" ; +} # end of dbint_handler + +# +# Do first time initialization at the startup +# of DB::DB +# +sub Initialize { + my ($fName) = @_ ; + + return if $DB::ptkdb::isInitialized ; + $DB::ptkdb::isInitialized = 1 ; + + $DB::window = new Devel::ptkdb ; + + $DB::window->do_user_init_files() ; + + + $DB::dbint_handler_save = $SIG{'INT'} unless $DB::sigint_disable ; # saves the old handler + $SIG{'INT'} = "DB::dbint_handler" unless $DB::sigint_disable ; + + # Save the file name we started up with + $DB::startupFname = $fName ; + + # Check for a 'restart' file + + if( $ENV{'PTKDB_RESTART_STATE_FILE'} && $Devel::ptkdb::DataDumperAvailable && -e $ENV{'PTKDB_RESTART_STATE_FILE'} ) { + ## + ## Restore expressions and breakpoints in state file + ## + $DB::window->restoreStateFile($ENV{'PTKDB_RESTART_STATE_FILE'}) ; + unlink $ENV{'PTKDB_RESTART_STATE_FILE'} ; # delete state file + + # print "restoring state from $ENV{'PTKDB_RESTART_STATE_FILE'}\n" ; + + $ENV{'PTKDB_RESTART_STATE_FILE'} = "" ; # clear entry + } + else { + &DB::restoreState($fName) if $Devel::ptkdb::DataDumperAvailable ; + } + +} # end of Initialize + +sub restoreState { + my($fName) = @_ ; + my ($stateFile, $files, $expr_list, $eval_saved_text, $main_win_geometry, $restoreName) ; + + $stateFile = makeFileSaveName($fName) ; + + if( -e $stateFile && -r $stateFile ) { + ($files, $expr_list, $eval_saved_text, $main_win_geometry) = $DB::window->get_state($stateFile) ; + &DB::restore_breakpoints_from_save($files) ; + $DB::window->{'expr_list'} = $expr_list if defined $expr_list ; + $DB::window->{eval_saved_text} = $eval_saved_text ; + + if ( $main_win_geometry ) { + # restore the height and width of the window + $DB::window->{main_window}->geometry($main_win_geometry) ; + } + } + +} # end of Restore State + +sub makeFileSaveName { + my ($fName) = @_ ; + my $saveName = $fName ; + + if( $saveName =~ /.p[lm]$/ ) { + $saveName =~ s/.pl$/.ptkdb/ ; + } + else { + $saveName .= ".ptkdb" ; + } + + return $saveName ; +} # end of makeFileSaveName + +sub save_state_file { + my($fname) = @_ ; + my($files, $d, $saveStr) ; + + $files = &DB::breakpoints_to_save() ; + + $d = Data::Dumper->new( [ $files, $DB::window->{'expr_list'}, "" ], + [ "files", "expr_list", "eval_saved_text" ] ) ; + + $d->Purity(1) ; + if( Data::Dumper->can('Dumpxs') ) { + $saveStr = $d->Dumpxs() ; + } else { + $saveStr = $d->Dump() ; + } + + local(*F) ; + open F, ">$fname" || die "Couldn't open file $fname" ; + + print F $saveStr || die "Couldn't write file" ; + + close F ; +} # end of save_state_file + +sub SaveState { + my($name_in) = @_ ; + my ($top, $entry, $okayBtn, $win) ; + my ($fname, $saveSub, $cancelSub, $saveName, $eval_saved_text, $d) ; + my ($files, $main_win_geometry); + # + # Create our default name + # + $win = $DB::window ; + + # + # Extract the height and width of our window + # + $main_win_geometry = $win->{main_window}->geometry ; + + if ( defined $win->{save_box} ) { + $win->{save_box}->raise ; + $win->{save_box}->focus ; + return ; + } + + $saveName = $name_in || makeFileSaveName($DB::startupFname) ; + + + + $saveSub = sub { + $win->{'event'} = 'null' ; + + my $saveStr ; + + delete $win->{save_box} ; + + if( exists $win->{eval_window} ) { + $eval_saved_text = $win->{eval_text}->get('0.0', 'end') ; + } + else { + $eval_saved_text = $win->{eval_saved_text} ; + } + + $files = &DB::breakpoints_to_save() ; + + $d = Data::Dumper->new( [ $files, $win->{'expr_list'}, $eval_saved_text, $main_win_geometry ], + [ "files", "expr_list", "eval_saved_text", "main_win_geometry"] ) ; + + $d->Purity(1) ; + if( Data::Dumper->can('Dumpxs') ) { + $saveStr = $d->Dumpxs() ; + } else { + $saveStr = $d->Dump() ; + } + + local(*F) ; + eval { + open F, ">$saveName" || die "Couldn't open file $saveName" ; + + print F $saveStr || die "Couldn't write file" ; + + close F ; + } ; + $win->DoAlert($@) if $@ ; + } ; # end of save sub + + $cancelSub = sub { + delete $win->{'save_box'} + } ; # end of cancel sub + + # + # Create a dialog + # + + $win->{'save_box'} = $win->simplePromptBox("Save Config?", $saveName, $saveSub, $cancelSub) ; + +} # end of SaveState + +sub RestoreState { + my ($top, $restoreSub) ; + + $restoreSub = sub { + $DB::window->restoreStateFile($Devel::ptkdb::promptString) ; + } ; + + $top = $DB::window->simplePromptBox("Restore Config?", makeFileSaveName($DB::startupFname), $restoreSub) ; + +} # end of RestoreState + +sub SetStepOverBreakPoint { + my ($offset) = @_ ; + $DB::step_over_depth = $DB::subroutine_depth + ($offset ? $offset : 0) ; +} # end of SetStepOverBreakPoint + +# +# NOTE: It may be logical and somewhat more economical +# lines of codewise to set $DB::step_over_depth_saved +# when we enter the subroutine, but this gets called +# for EVERY callable line of code in a program that +# is being debugged, so we try to save every line of +# execution that we can. +# +sub isBreakPoint { + my ($fname, $line, $package) = @_ ; + my ($brkPt) ; + + if ( $DB::single && ($DB::step_over_depth < $DB::subroutine_depth) && ($DB::step_over_depth > 0) && !$DB::on) { + $DB::single = 0 ; + return 0 ; + } + # + # doing a step over/in + # + + if( $DB::single || $DB::signal ) { + $DB::single = 0 ; + $DB::signal = 0 ; + $DB::subroutine_depth = $DB::subroutine_depth ; + return 1 ; + } + # + # 1st Check to see if there is even a breakpoint there. + # 2nd If there is a breakpoint check to see if it's check box control is 'on' + # 3rd If there is any kind of expression, evaluate it and see if it's true. + # + $brkPt = &DB::getdbline($fname, $line) ; + + return 0 if( !$brkPt || !$brkPt->{'value'} || !breakPointEvalExpr($brkPt, $package) ) ; + + &DB::cleardbline($fname, $line) if( $brkPt->{'type'} eq 'temp' ) ; + + $DB::subroutine_depth = $DB::subroutine_depth ; + + return 1 ; +} # end of isBreakPoint + +# +# Check the breakpoint expression to see if it +# is true. +# +sub breakPointEvalExpr { + my ($brkPt, $package) = @_ ; + my (@result) ; + + return 1 unless $brkPt->{expr} ; # return if there is no expression + + no strict ; + + @result = &DB::dbeval($package, $brkPt->{'expr'}) ; + + use strict ; + + $DB::window->DoAlert($@) if $@ ; + + return $result[0] or @result ; # we could have a case where the 1st element is undefined + # but subsequent elements are defined + +} # end of breakPointEvalExpr + +# +# Evaluate the given expression, return the result. +# MUST BE CALLED from within DB::DB in order for it +# to properly interpret the vars +# +sub dbeval { + my($ptkdb__package, $ptkdb__expr) = @_ ; + my(@ptkdb__result, $ptkdb__str, $ptkdb__saveW) ; + my(@ptkdb_args) ; + + no strict ; + $ptkdb__saveW = $^W ; # save the state of the "warning"(-w) flag + $^W = 0 ; + + # + # This substitution is done so that + # we return HASH, as opposed to an ARRAY. + # An expression of %hash results in a + # list of key/value pairs. + # + + $ptkdb__expr =~ s/^\s*%/\\%/o ; + + @_ = @DB::saved_args ; # replace @_ arg array with what we came in with + + @ptkdb__result = eval <<__EVAL__ ; + + + \$\@ = \$DB::save_err ; + + package $ptkdb__package ; + + $ptkdb__expr ; + +__EVAL__ + + @ptkdb__result = ("ERROR ($@)") if $@ ; + + $^W = $ptkdb__saveW ; # restore the state of the "warning"(-w) flag + + use strict ; + + return @ptkdb__result ; +} # end of dbeval + +# +# Call back we give to our 'quit' button +# and binding to the WM_DELETE_WINDOW protocol +# to quit the debugger. +# +sub dbexit { + exit ; +} # end of dbexit + +# +# This is the primary entry point for the debugger. When a perl program +# is parsed with the -d(in our case -d:ptkdb) option set the parser will +# insert a call to DB::DB in front of every excecutable statement. +# +# Refs: Progamming Perl 2nd Edition, Larry Wall, O'Reilly & Associates, Chapter 8 +# +sub DB { + @DB::saved_args = @_ ; # save arg context + $DB::save_err = $@ ; # save value of $@ + my ($package, $filename, $line) = caller ; + my ($stop, $cnt) ; + + unless( $DB::ptkdb::isInitialized ) { + return if( $filename ne $0 ) ; # not in our target file + + &DB::Initialize($filename) ; + } + + if (!isBreakPoint($filename, $line, $package) ) { + $DB::single = 0 ; + $@ = $DB::save_err ; + return ; + } + + + + if ( !$DB::window ) { # not setup yet + $@ = $DB::save_err ; + return ; + } + + $DB::window->setup_main_window() unless $DB::window->{'main_window'} ; + + $DB::window->EnterActions() ; + + my ($saveP) ; + $saveP = $^P ; + $^P = 0 ; + + $DB::on = 1 ; + +# +# The user can specify this variable in one of the startup files, +# this will make the debugger run right after startup without +# the user having to press the 'run' button. +# + if( $DB::no_stop_at_start ) { + $DB::no_stop_at_start = 0 ; + $DB::on = 0 ; + $@ = $DB::save_err ; + return ; + } + + if( !$DB::sigint_disable ) { + $SIG{'INT'} = $DB::dbint_handler_save if $DB::dbint_handler_save ; # restore original signal handler + $SIG{'INT'} = "DB::dbexit" unless $DB::dbint_handler_save ; + } + + #$DB::window->{main_window}->raise() ; # bring us to the top make sure OUR event loop runs + $DB::window->{main_window}->focus() ; + + $DB::window->set_file($filename, $line) ; + # + # Refresh the exprs to see if anything has changed + # + updateExprs($package) ; + + # + # Update subs Page if necessary + # + $cnt = scalar keys %DB::sub ; + if ( $cnt != $DB::window->{'subs_list_cnt'} && $DB::window->{'subs_page_activated'} ) { + $DB::window->fill_subs_page() ; + $DB::window->{'subs_list_cnt'} = $cnt ; + } + # + # Update the subroutine stack menu + # + $DB::window->refresh_stack_menu() ; + + $DB::window->{run_flag} = 1 ; + + my ($evt, @result, $r) ; + + for( ; ; ) { + # + # we wait here for something to do + # + $evt = $DB::window->main_loop() ; + + last if( $evt eq 'step' ) ; + + $DB::single = 0 if ($evt eq 'run' ) ; + + if ($evt eq 'balloon_eval' ) { + $DB::window->code_motion_eval(&DB::dbeval($package, $DB::window->{'balloon_expr'})) ; + next ; + } + + if ( $evt eq 'qexpr' ) { + my $str ; + @result = &DB::dbeval($package, $DB::window->{'qexpr'}) ; + $DB::window->{'quick_entry'}->delete(0, 'end') ; # clear old text + if (exists $DB::window->{'quick_dumper'}) { + $DB::window->{'quick_dumper'}->Reset() ; + $DB::window->{'quick_dumper'}->Values( [ $#result == 0 ? @result : \@result ] ) ; + if( $DB::window->{'quick_dumper'}->can('Dumpxs') ) { + $str = $DB::window->{'quick_dumper'}->Dumpxs() ; + } + else { + $str = $DB::window->{'quick_dumper'}->Dump() ; + } + } + else { + $str = "@result" ; + } + $DB::window->{'quick_entry'}->insert(0, $str) ; #enter the text + $DB::window->{'quick_entry'}->selectionRange(0, 'end') ; # select it + $evt = 'update' ; # force an update on the expressions + } + + if( $evt eq 'expr' ) { + # + # Append the new expression to the list + # but first check to make sure that we don't + # already have it. + # + + if ( grep $_->{'expr'} eq $DB::window->{'expr'}, @{$DB::window->{'expr_list'}} ) { + $DB::window->DoAlert("$DB::window->{'expr'} is already listed") ; + next ; + } + + @result = &DB::dbeval($package, $DB::window->{expr}) ; + + if( scalar @result == 1 ) { + $r = $DB::window->insertExpr([ $result[0] ], $DB::window->{'data_list'}, $result[0], $DB::window->{'expr'}, $Devel::ptkdb::expr_depth) ; + } + else { + $r = $DB::window->insertExpr([ \@result ], $DB::window->{'data_list'}, \@result, $DB::window->{'expr'}, $Devel::ptkdb::expr_depth) ; + } + + # + # $r will be 1 if the expression was added succesfully, 0 if not, + # and it if wasn't added sucessfully it won't be reevalled the + # next time through. + # + push @{$DB::window->{'expr_list'}}, { 'expr' => $DB::window->{'expr'}, 'depth' => $Devel::ptkdb::expr_depth } if $r ; + + next ; + } + if( $evt eq 'update' ) { + updateExprs($package) ; + next ; + } + if( $evt eq 'reeval' ) { + # + # Reevaluate the contents of the expression eval window + # + my $txt = $DB::window->{'eval_text'}->get('0.0', 'end') ; + my @result = &DB::dbeval($package, $txt) ; + + $DB::window->updateEvalWindow(@result) ; + + next ; + } + last ; + } + $^P = $saveP ; + $SIG{'INT'} = "DB::dbint_handler" unless $DB::sigint_disable ; # set our signal handler + + $DB::window->LeaveActions() ; + + $@ = $DB::save_err ; + $DB::on = 0 ; + } # end of DB + +# +# This is another place where we'll try and keep the +# code as 'lite' as possible to prevent the debugger +# from slowing down the user's application +# +# When a perl program is parsed with the -d(in our case a -d:ptkdb) option +# the parser will route all subroutine calls through here, setting $DB::sub +# to the name of the subroutine to be called, leaving it to the debugger to +# make the actual subroutine call and do any pre or post processing it may +# need to do. In our case we take the opportunity to track the depth of the call +# stack so that we can update our 'Stack' menu when we stop. +# +# Refs: Progamming Perl 2nd Edition, Larry Wall, O'Reilly & Associates, Chapter 8 +# +# + sub sub { + my ($result, @result) ; +# +# See NOTES(1) +# + if( wantarray ) { + $DB::subroutine_depth += 1 unless $DB::on ; + $DB::single = 0 if ( ($DB::step_over_depth < $DB::subroutine_depth) && ($DB::step_over_depth >= 0) && !$DB::on) ; + + no strict ; # otherwise perl gripes about calling the sub by the reference + @result = &$DB::sub ; # call the subroutine by name + use strict ; + + $DB::subroutine_depth -= 1 unless $DB::on ; + $DB::single = 1 if ($DB::step_over_depth >= $DB::subroutine_depth && !$DB::on) ; + return @result ; + } + else { + $DB::subroutine_depth += 1 unless $DB::on ; + $DB::single = 0 if ( ($DB::step_over_depth < $DB::subroutine_depth) && ($DB::step_over_depth >= 0) && !$DB::on) ; + + no strict ; # otherwise perl gripes about calling the sub by the reference + $result = &$DB::sub ; # call the subroutine by name + use strict ; + + $DB::subroutine_depth -= 1 unless $DB::on ; + $DB::single = 1 if ($DB::step_over_depth >= $DB::subroutine_depth && !$DB::on) ; + return $result ; + } + + } # end of sub + +1 ; # return true value |