apave.tcl
README.md

apave.tcl

  • apave.tcl
  • Independent procs
    • iswindows ::iswindows : Checks for "platform is MS Windows".
    • isunix ::isunix : Checks for "platform is Unix".
    • isKDE ::isKDE : Checks for "desktop is KDE".
    • asKDE ::asKDE : Checks for DE behaving as weird as KDE.
  • apave NS
    • obj obj : Calls a method of APave class. com - a method args - arguments of the method It can (and must) be used only for temporary tasks. For persistent tasks, use a "normal" apave object. Returns the command's result.
    • None None : Useful when to do nothing is better than to do something.
    • autoexec autoexec : Imitates Tcl's auto_execok. comm - a command to find ext - file's extension (for Windows) If it doesn't get the command from Tcl's auto_execok, it tries to knock at its file by itself.
    • openDoc openDoc : Opens a document. url - document's file name, www link, e-mail etc.
    • countChar countChar : Counts a character in a string. str - a string ch - a character Returns a number of non-escaped occurences of character *ch* in string *str*. See also: [wiki.tcl-lang.org](https://wiki.tcl-lang.org/page/Reformatting+Tcl+code+indentation)
    • traceRemove traceRemove : Cancels tracing of a variable. v - variable's name
    • initBaltip initBaltip : Initializes baltip package.
  •   Integers
    • getN getN : Gets a number from a string sn - string containing a number defn - default value when sn is not a number min - minimal value allowed max - maximal value allowed
    • p+ p+ : Sums two text positions straightforward: lines & columns separately. p1 - 1st position p2 - 2nd position The lines may be with "-". Reasons for this: 1. expr $p1+$p2 doesn't work, e.g. 309.10+1.4=310.5 instead of 310.14 2. do it without a text widget's path (for text's arithmetic)
    • pint pint : Gets int part of text position, e.g. "4" for "4.end". pos - position in text
    • intInRange intInRange : Checks whether an integer is in min-max range. int - the integer min - minimum of the range max - maximum of the range
    • IsRoundInt IsRoundInt : Checks whether an integer equals roundly to other integer. i1 - integer to compare i2 - integer to be compared (rounded) to i1
  •   Lists, arrays
    • lsearchFile lsearchFile : Searches a file name in a list, using normalized file names. flist - list of file names fname - file name to find Returns an index of found file name or -1 if it's not found.
    • RestoreArray RestoreArray : Tries restoring an array 1:1. arName - fully qualified array name arSave - saved array's value (got with "array get") At restoring, new items of $arName are deleted and existing items are updated, so that after restoring *array get $arName* is equal to $arSave. Note: "array unset $arName *; array set $arName $arSave" doesn't ensure this equality.
    • EnsureArray EnsureArray : Ensures restoring an array at calling a proc. arName - fully qualified array name args - proc name & arguments
    • PushInList PushInList : Pushes an item in a list: deletes an old instance, inserts a new one. listName - the list's variable name item - item to push pos - position in the list to push in max - maximum length of the list
  •   Widgets
    • checkGeometry checkGeometry : Checks a window's geometry. geo - the geometry Returns a "normalized" geometry (+0+0 if input not correct).
    • repaintWindow repaintWindow : Shows a window and, optionally, focuses on a widget of it. win - the window's path wfoc - the widget's path or a command to get it Returns yes, if the window is shown successfully.
    • rootModalWindow rootModalWindow : Gets a parent modal window for a given one. pwin - default parent
    • splitGeometry splitGeometry : Gets widget's geometry components. geom - geometry X - default X-coordinate Y - default Y-coordinate Returns a list of width, height, X and Y (coordinates are always with + or -) and also a flag "negative coordinates, calculated from bottom right".
    • focusFirst focusFirst : Sets a focus on a first widget of a parent widget. w - the parent widget dofocus - if no, means "only return the widget's path" res - used for recursive call Returns a path to a focused widget or "".
    • focusedWidget focusedWidget : Gets a flag "is a widget can be focused". w - widget's path
    • MouseOnWidget MouseOnWidget : Places the mouse pointer on a widget. w1 - the widget's path
    • CursorAtEnd CursorAtEnd : Sets the cursor at the end of a field. w - the field's path
    • focusByForce focusByForce : Focuses a widget. foc - widget's path
    • KeyAccelerator KeyAccelerator : Returns a key accelerator. acc - key name, may contain 2 items (e.g. Control-D Control-d)
    • InvertBg InvertBg : Gets a "inverted" color (white/black) for an color. clr - color (#hhh or #hhhhhh) B - "black" color W - "white" color Returns a list of "black/white" and normalized input color
  •     Blinking widgets
    • blinkWidget blinkWidget
    • blinkWidgetImage blinkWidgetImage : Makes a widget's image blink. w - widget's path img1 - main image img2 - flashed image cnt - count of flashes ms - millisec between flashes
  •   File names
    • HomeDir HomeDir : For Tcl 9.0 & Windows: gets a home directory ("~").
    • checkHomeDir checkHomeDir : For Tcl 9.0 & Windows: checks a command for "~".
    • UnixPath UnixPath : Makes a path "unix-like" to be good for Tcl. path - the path
    • NormalizeName NormalizeName : Removes spec.characters from a name (sort of normalizing it). name - the name
    • NormalizeFileName NormalizeFileName : Removes spec.characters from a file/dir name (sort of normalizing it). name - the name of file/dir
    • FileTail FileTail : Extracts a tail path from a full file path. E.g. FileTail /a/b /a/b/cd/ef => cd/ef basepath - base path fullpath - full path
    • FileRelativeTail FileRelativeTail : Gets a base relative path. E.g. FileRelativeTail /a/b /a/b/cd/ef => ../ef basepath - base path fullpath - full path
  •   Borrowed from BWidget
    • place place
  •   EONS apave
  • APave
    • constructor constructor : Creates APave object. win - window's name (path) args - additional arguments
    • destructor destructor : Clears variables used in the object.
    • initInput initInput : Initializes input and clears variables made in previous session.
    • varInput varInput : Gets variables made and filled in a previous session as a list of "varname varvalue" pairs where varname is of form: namespace::var$widgetname.
    • valueInput valueInput : Gets input variables' values.
    • input input : Makes and runs an input dialog. icon - icon (omitted if equals to "") ttl - title of window iopts - list of widgets and their attributes args - list of dialog's attributes The `iopts` contains lists of three items: name - name of widgets prompt - prompt for entering data valopts - value options The `valopts` is a list specific for a widget's type, however a first item of `valopts` is always an initial input value.
    • vieweditFile vieweditFile : Views or edits a file. fname - name of file prepcom - a command performing before and after creating a dialog args - additional options It's a sort of stub for calling *editfile* method. See also: editfile
    • editfile editfile : Edits or views a file with a set of main colors fname - name of file fg - foreground color of text widget bg - background color of text widget cc - caret's color of text widget prepcom - a command performing before and after creating a dialog args - additional options (`-readonly 1` for viewing the file). If *fg* isn't empty, all three colors are used to color a text. See also: [aplsimple.github.io](https://aplsimple.github.io/en/tcl/pave/index.html)
    • onTop onTop : Sets -topmost attribute for windows or gets a list of topmost windows. wpar - parent window's path top - -topmost attribute's value wtoplist - list of windows to process res - used to get the result Returns a list of "topmost=$top" windows found on $wpar path.
  • EONS
  • EOF

apave

A library for GUI development with Tcl/Tk.

Docs:

Full description

apave.tcl
###########################################################
# Name:    apave.tcl
# Author:  Alex Plotnikov  (aplsimple@gmail.com)
# Date:    12/09/2021
# Brief:   Handles APave class creating input dialogs.
# License: MIT.
###########################################################

package require Tk
package provide apave 4.6.1

source [file join [file dirname [info script]] apavedialog.tcl]

# ________________________ Independent procs _________________________ #

proc ::iswindows {} {
  # Checks for "platform is MS Windows".

  expr {$::tcl_platform(platform) eq {windows}}
}

proc ::isunix {} {
  # Checks for "platform is Unix".

  expr {$::tcl_platform(platform) eq {unix}}
}

proc ::isKDE {} {
  # Checks for "desktop is KDE".

  expr {[info exists ::env(XDG_CURRENT_DESKTOP)] && $::env(XDG_CURRENT_DESKTOP) eq {KDE}}
}

proc ::asKDE {} {
  # Checks for DE behaving as weird as KDE.

  expr {[::isKDE] && ![package vsatisfies [package require Tcl] 8.6.11-]}
}
# ________________________ apave NS _________________________ #

namespace eval ::apave {

namespace export obj openDoc textsplit focusByForce *TextFile undo* *Option*

mainWindowOfApp .

variable _OBJ_ {}

proc obj {com args} {
  # Calls a method of APave class.
  #   com - a method
  #   args - arguments of the method
  # It can (and must) be used only for temporary tasks.
  # For persistent tasks, use a "normal" apave object.
  # Returns the command's result.

  variable _OBJ_
  if {$_OBJ_ eq {}} {set _OBJ_ [::apave::APave new]}
  if {[set exported [expr {$com eq "EXPORT"}]]} {
    set com [lindex $args 0]
    set args [lrange $args 1 end]
    oo::objdefine $_OBJ_ "export $com"
  }
  set res [$_OBJ_ $com {*}$args]
  if {$exported} {
    oo::objdefine $_OBJ_ "unexport $com"
  }
  return $res
}
#_______________________

proc None {args} {
  # Useful when to do nothing is better than to do something.

}
#_______________________

proc autoexec {comm {ext ""}} {
  # Imitates Tcl's auto_execok.
  #   comm - a command to find
  #   ext - file's extension (for Windows)
  # If it doesn't get the command from Tcl's auto_execok,
  # it tries to knock at its file by itself.

  if {$ext ne {} && [::iswindows]} {append comm $ext}
  set res [auto_execok $comm]
  if {$res eq {} && [file exists $comm]} {
    set res $comm
  }
  return $res
}
#_______________________

proc openDoc {url} {
  # Opens a document.
  #   url - document's file name, www link, e-mail etc.

  set commands {xdg-open open start}
  foreach opener $commands {
    if {$opener eq "start"} {
      set command [list {*}[auto_execok start] {}]
    } else {
      set command [auto_execok $opener]
    }
    if {[string length $command]} {
      break
    }
  }
  if {[string length $command] == 0} {
    puts "ERROR: couldn't find any opener"
  }
  # remove the tailing " &" (as e_menu can set)
  set url [string trimright $url]
  if {[string match "* &" $url]} {set url [string range $url 0 end-2]}
  set url [string trim $url]
  if {[catch {exec -- {*}$command $url &} error]} {
    puts "ERROR: couldn't execute '$command':\n$error"
  }
}
#_______________________

proc countChar {str ch} {
  # Counts a character in a string.
  #   str - a string
  #   ch - a character
  #
  # Returns a number of non-escaped occurences of character *ch* in
  # string *str*.
  #
  # See also:
  # [wiki.tcl-lang.org](https://wiki.tcl-lang.org/page/Reformatting+Tcl+code+indentation)

  set icnt 0
  while {[set idx [string first $ch $str]] >= 0} {
    set backslashes 0
    set nidx $idx
    while {[string equal [string index $str [incr nidx -1]] \\]} {
      incr backslashes
    }
    if {$backslashes % 2 == 0} { incr icnt }
    set str [string range $str [incr idx] end]
  }
  return $icnt
}
#_______________________

proc traceRemove {v} {
  # Cancels tracing of a variable.
  #   v - variable's name

  foreach t [trace info variable $v] {
    lassign $t o c
    trace remove variable $v $o $c
  }
}
#_______________________

proc initBaltip {} {
  # Initializes baltip package.

  if {[info command ::baltip] eq {}} {
    if {$::apave::ISBALTIP} {
      source [file join $::apave::SRCDIR baltip baltip.tcl]
    } else {
      # disabling baltip facilities with stub proc (no source "baltip.src")
      namespace eval ::baltip {
        variable expproc [list configure cget tip update hide repaint \
          optionlist tippath clear sleep showBalloon showTip]
        foreach _ $expproc {
        ; proc $_ {args} {return {}}
          namespace export $_
        }
        namespace ensemble create
        namespace eval my {
        ; proc BindToEvent {args} {}
        }
      }
    }
  }
}

## ________________________ Integers _________________________ ##


proc getN {sn {defn 0} {min ""} {max ""}} {
  # Gets a number from a string
  #   sn - string containing a number
  #   defn - default value when sn is not a number
  #   min - minimal value allowed
  #   max - maximal value allowed

  if {$sn eq "" || [catch {set sn [expr {$sn}]}]} {set sn $defn}
  if {$max ne ""} {
    set sn [expr {min($max,$sn)}]
  }
  if {$min ne ""} {
    set sn [expr {max($min,$sn)}]
  }
  return $sn
}
#_______________________

proc p+ {p1 p2} {
  # Sums two text positions straightforward: lines & columns separately.
  #   p1 - 1st position
  #   p2 - 2nd position
  # The lines may be with "-".
  # Reasons for this:
  #  1. expr $p1+$p2 doesn't work, e.g. 309.10+1.4=310.5 instead of 310.14
  #  2. do it without a text widget's path (for text's arithmetic)

  lassign [split $p1 .] l11 c11
  lassign [split $p2 .] l21 c21
  foreach n {l11 c11 l21 c21} {
    if {![string is digit -strict [string trimleft [set $n] -]]} {set $n 0}
  }
  return [incr l11 $l21].[incr c11 $c21]
}
#_______________________

proc pint {pos} {
  # Gets int part of text position, e.g. "4" for "4.end".
  #   pos - position in text

  if {[set i [string first . $pos]]>0} {incr i -1} {set i end}
  expr {int([string range $pos 0 $i])}
}
#_______________________

proc intInRange {int min max} {
  # Checks whether an integer is in min-max range.
  #   int - the integer
  #   min - minimum of the range
  #   max - maximum of the range

  expr {[string is integer -strict $int] && $int>=$min && $int<=$max}
}
#_______________________

proc IsRoundInt {i1 i2} {
  # Checks whether an integer equals roundly to other integer.
  #   i1 - integer to compare
  #   i2 - integer to be compared (rounded) to i1

  expr {$i1>($i2-3) && $i1<($i2+3)}
}

## _______________________ Lists, arrays _______________________ ##

proc lsearchFile {flist fname} {
  # Searches a file name in a list, using normalized file names.
  #   flist - list of file names
  #   fname - file name to find
  # Returns an index of found file name or -1 if it's not found.

  set i 0
  set fname [file normalize $fname]
  foreach fn $flist {
    if {[file normalize $fn] eq $fname} {
      return $i
    }
    incr i
  }
  return -1
}
#_______________________

proc RestoreArray {arName arSave} {
  # Tries restoring an array 1:1.
  #   arName - fully qualified array name
  #   arSave - saved array's value (got with "array get")
  # At restoring, new items of $arName are deleted and existing items are updated,
  # so that after restoring *array get $arName* is equal to $arSave.
  # Note: "array unset $arName *; array set $arName $arSave" doesn't ensure this equality.

  set ar $arName
  array set artmp $arSave
  set tmp1 [array names artmp]
  set tmp2 [array names $arName]
  foreach n $tmp2 {
    if {$n ni $tmp1} {unset [set ar]($n)} {set [set ar]($n) $artmp($n)}
  }
  foreach n $tmp1 {
    # deleted items can break 1:1 equality (not the case with alited)
    if {$n ni $tmp2} {set [set ar]($n) $artmp($n)}
  }
}
#_______________________

proc EnsureArray {arName args} {
  # Ensures restoring an array at calling a proc.
  #   arName - fully qualified array name
  #   args - proc name & arguments

  set arSave [array get $arName]
  {*}$args
  RestoreArray $arName $arSave
}
#_______________________

proc PushInList {listName item {pos 0} {max 16}} {
  # Pushes an item in a list: deletes an old instance, inserts a new one.
  #   listName - the list's variable name
  #   item - item to push
  #   pos - position in the list to push in
  #   max - maximum length of the list

  upvar $listName ln
  if {[set i [lsearch -exact $ln $item]]>-1} {
    set ln [lreplace $ln $i $i]
  }
  set ln [linsert $ln $pos $item]
  catch {set ln [lreplace $ln $max end]}
}

## ________________________ Widgets _________________________ ##

proc checkGeometry {geo} {
  # Checks a window's geometry.
  #   geo - the geometry
  # Returns a "normalized" geometry (+0+0 if input not correct).

  if {!([regexp {^\d+x\d+\+-?\d+\+-?\d+$} $geo] ||
  [regexp {^\+-?\d+\+-?\d+$} $geo] || [regexp {^\d+x\d+$} $geo])} {
    set geo +0+0
  }
  return $geo
}
#_______________________

proc repaintWindow {win {wfoc ""}} {
  # Shows a window and, optionally, focuses on a widget of it.
  #   win - the window's path
  #   wfoc - the widget's path or a command to get it
  # Returns yes, if the window is shown successfully.

  if {[winfo exists $win]} {
    # esp. for KDE
    if {[isKDE]} { ;# KDE is KDE, Tk is Tk, and never the twain shall meet
      wm withdraw $win
      wm deiconify $win
      wm attributes $win -topmost [wm attributes $win -topmost]
    }
    update
    if {$wfoc ne {}} {
      catch {set wfoc [{*}$wfoc]}
      focus $wfoc
    }
    return yes
  }
  return no
}
#_______________________

proc rootModalWindow {pwin} {
  # Gets a parent modal window for a given one.
  #   pwin - default parent

  set root $pwin
  foreach w [winfo children $pwin] {
    if {[winfo ismapped $w] && [InfoFind $w yes] ne {}} {
      set root [winfo toplevel $w]
    }
  }
  return $root
}
#_______________________

proc splitGeometry {geom {X +0} {Y +0}} {
  # Gets widget's geometry components.
  #   geom - geometry
  #   X - default X-coordinate
  #   Y - default Y-coordinate
  # Returns a list of width, height, X and Y (coordinates are always with + or -)
  # and also a flag "negative coordinates, calculated from bottom right".

  lassign [split $geom x+-] w h
  lassign [regexp -inline -all {([+-][[:digit:]]+)} $geom] -> x y
  if {$geom ne {}} {
    if {$x in {"" 0} || [catch {expr {$x+0}}]} {set x $X}
    if {$y in {"" 0} || [catch {expr {$y+0}}]} {set y $Y}
  }
  set neg [expr {[string first - $geom]>=0 && [string first + $geom]<0}]
  list $w $h $x $y $neg
}
#_______________________

proc focusFirst {w {dofocus yes} {res {}}} {
  # Sets a focus on a first widget of a parent widget.
  #  w - the parent widget
  #  dofocus - if no, means "only return the widget's path"
  #  res - used for recursive call
  # Returns a path to a focused widget or "".

  if {$w ne {}} {
    foreach w [winfo children $w] {
      if {[focusedWidget $w]} {
        if {$dofocus} {after 200 "catch {focus -force $w}"}
        return $w
      } else {
        if {[set res [focusFirst $w $dofocus]] ne {}} break
      }
    }
  }
  return $res
}
#_______________________

proc focusedWidget {w} {
  # Gets a flag "is a widget can be focused".
  #   w - widget's path

  set wclass [string tolower [winfo class $w]]
  foreach c [list entry text button box list view] {
    if {[string match *$c $wclass]} {
      if {[catch {set state [$w cget -state]}]} {set state normal}
      if {$state ne {disabled}} {
        if {[catch {set focus [$w cget -takefocus]}]} {set focus no}
        return [expr {![string is boolean -strict $focus] || $focus}]
      }
      break
    }
  }
  return no
}
#_______________________

proc MouseOnWidget {w1} {
  # Places the mouse pointer on a widget.
  #   w1 - the widget's path

  update
  set w2 [winfo parent $w1]
  set w3 [winfo parent $w2]
  lassign [split [winfo geometry $w1] +x] w h x1 y1
  lassign [split [winfo geometry $w2] +x] - - x2 y2
  event generate $w3 <Motion> -warp 1 \
    -x [expr {$x1+$x2+int($w/2)}] -y [expr {$y1+$y2+int($h/2)}]
}
#_______________________

proc CursorAtEnd {w} {
  # Sets the cursor at the end of a field.
  #   w - the field's path

  focus $w
  $w selection clear
  $w icursor end
}
#_______________________

proc focusByForce {foc {cnt 10}} {
  # Focuses a widget.
  #   foc - widget's path

  if {[incr cnt -1]>0} {
    after idle after 5 ::apave::focusByForce $foc $cnt
  } else {
    catch {focus -force [winfo toplevel $foc]; focus $foc}
  }
}
#_______________________

proc KeyAccelerator {acc} {
  # Returns a key accelerator.
  #   acc - key name, may contain 2 items (e.g. Control-D Control-d)

  set acc [lindex $acc 0]
  string map {Control Ctrl - + bracketleft [ bracketright ]} $acc
}
#_______________________

proc InvertBg {clr {B #000000} {W #FFFFFF}} {
  # Gets a "inverted" color (white/black) for an color.
  #   clr - color (#hhh or #hhhhhh)
  #   B - "black" color
  #   W - "white" color
  # Returns a list of "black/white" and normalized input color

  if {[string length $clr]==4} {
    lassign [split $clr {}] -> r g b
    set clr #$r$r$g$g$b$b
  }
  lassign [winfo rgb . $clr] r g b
  if {($r%256+$b%256)<15 && ($g%256)>180 || $r+1.5*$g+0.5*$b > 100000} {
    set res $B
  } else {
    set res $W
  }
  list $res $clr
}

### ________________________ Blinking widgets _________________________ ###

proc blinkWidget {w {fg #000} {bg #fff} {fg2 {}} {bg2 red} \
  {pause 1000} {count -1} {mode 1}} {
  # Makes a widget blink.
  #   w - the widget's path
  #   fg - normal foreground color
  #   bg - normal background color
  #   fg2 - blinking foreground color (if {}, stops the blinking)
  #   bg2 - blinking background color
  #   pause - pause in millisec between blinkings
  #   count - means how many times do blinking
  #   mode - for recursive calls

  if {![winfo exists $w]} return
  if {$count==0 || $fg2 eq {}} {
    catch {after cancel $::apave::BLINKWIDGET1}
    catch {after cancel $::apave::BLINKWIDGET2}
    after idle "$w configure -foreground $fg; $w configure -background $bg"
  } elseif {$mode==1} {
    incr count -1
    $w configure -foreground $fg2
    $w configure -background $bg2
    set ::apave::BLINKWIDGET1 [after \
      $pause ::apave::blinkWidget $w $fg $bg $fg2 $bg2 $pause $count 2]
  } elseif {$mode==2} {
    $w configure -foreground $fg
    $w configure -background $bg
    set ::apave::BLINKWIDGET2 [after \
      $pause ::apave::blinkWidget $w $fg $bg $fg2 $bg2 $pause $count 1]
  }
}
#_______________________

proc blinkWidgetImage {w img1 {img2 alimg_none} {cnt 6} {ms 100}} {
  # Makes a widget's image blink.
  #   w - widget's path
  #   img1 - main image
  #   img2 - flashed image
  #   cnt - count of flashes
  #   ms - millisec between flashes

  set imgcur $img1
  if {$cnt>0} {
    if {$cnt % 2} {set imgcur $img2}
    after $ms "::apave::blinkWidgetImage $w $img1 $img2 [incr cnt -1] $ms"
  }
  $w configure -image $imgcur
}

## ________________________ File names _________________________ ##

proc HomeDir {} {
  # For Tcl 9.0 & Windows: gets a home directory ("~").

  if {[catch {set hd [file home]}]} {
    if {[info exists ::env(HOME)]} {set hd $::env(HOME)} {set hd ~}
  }
  return $hd
}
#_______________________

proc checkHomeDir {com} {
  # For Tcl 9.0 & Windows: checks a command for "~".

  set hd [HomeDir]
  set com [string map [list { ~/} " $hd/" \"~/ \"$hd/ '~/ '$hd/ \\n~/ \\n$hd/ \n~/ \n$hd/ \{~/ \{$hd/] $com]
  if {[string match ~/* $com]} {set com $hd[string range $com 1 end]}
  return $com
}
#_______________________

proc UnixPath {path} {
  # Makes a path "unix-like" to be good for Tcl.
  #   path - the path

  set path [string trim $path "\{\}"]  ;# possibly braced if contains spaces
  set path [string map [list \\ / %H [HomeDir]] $path]
  checkHomeDir $path
}
#_______________________

proc NormalizeName {name} {
  # Removes spec.characters from a name (sort of normalizing it).
  #   name - the name

  string map [list \\ {} \{ {} \} {} \[ {} \] {} \t {} \n {} \r {} \" {}] $name
}
#_______________________

proc NormalizeFileName {name} {
  # Removes spec.characters from a file/dir name (sort of normalizing it).
  #   name - the name of file/dir

  set name [string trim $name]
  string map [list \
    * _ ? _ ~ _ / _ \\ _ \{ _ \} _ \[ _ \] _ \t _ \n _ \r _ \
    | _ < _ > _ & _ , _ : _ \; _ \" _ ' _ ` _] $name
}
#_______________________

proc FileTail {basepath fullpath} {
  # Extracts a tail path from a full file path.
  # E.g. FileTail /a/b /a/b/cd/ef => cd/ef
  #   basepath - base path
  #   fullpath - full path

  set lbase [file split $basepath]
  set lfull [file split $fullpath]
  set ll [expr {[llength $lfull] - [llength $lbase] - 1}]
  if {$ll>-1} {
    return [file join {*}[lrange $lfull end-$ll end]]
  }
  return {}
}
#_______________________

proc FileRelativeTail {basepath fullpath} {
  # Gets a base relative path.
  # E.g. FileRelativeTail /a/b /a/b/cd/ef => ../ef
  #   basepath - base path
  #   fullpath - full path

  set tail [FileTail $basepath $fullpath]
  set lev [llength [file split $tail]]
  set base {}
  for {set i 1} {$i<$lev} {incr i} {append base ../}
  append base [file tail $tail]
}

## ________________________ Borrowed from BWidget _________________________ ##

#  Command BWidget::place ----> apave::place
#
# Notes:
#  For Windows systems with more than one monitor the available screen area may
#  have negative positions. Geometry settings with negative numbers are used
#  under X to place wrt the right or bottom of the screen. On windows, Tk
#  continues to do this. However, a geometry such as 100x100+-200-100 can be
#  used to place a window onto a secondary monitor. Passing the + gets Tk
#  to pass the remainder unchanged so the Windows manager then handles -200
#  which is a position on the left hand monitor.
#  I've tested this for left, right, above and below the primary monitor.
#  Currently there is no way to ask Tk the extent of the Windows desktop in
#  a multi monitor system. Nor what the legal co-ordinate range might be.
#

proc place { path w h args } {

  update idletasks

  # If the window is not mapped, it may have any current size.
  # Then use required size, but bound it to the screen width.
  # This is mostly inexact, because any toolbars will still be removed
  # which may reduce size.
  if { $w == 0 && [winfo ismapped $path] } {
    set w [winfo width $path]
  } else {
    if { $w == 0 } {
      set w [winfo reqwidth $path]
    }
    set vsw [winfo vrootwidth  $path]
    if { $w > $vsw } { set w $vsw }
  }

  if { $h == 0 && [winfo ismapped $path] } {
    set h [winfo height $path]
  } else {
    if { $h == 0 } {
      set h [winfo reqheight $path]
    }
    set vsh [winfo vrootheight $path]
    if { $h > $vsh } { set h $vsh }
  }

  set arglen [llength $args]
  if { $arglen > 3 } {
    return -code error "apave::place: bad number of argument"
  }

  if { $arglen > 0 } {
    set where [lindex $args 0]
    set list  [list at center left right above below]
    set idx   [lsearch $list $where]
    if { $idx == -1 } {
      return -code error "apave::place: bad position: $where $list"
    }
    if { $idx == 0 } {
      set err [catch {
        # purposely removed the {} around these expressions - [PT]
        set x [expr int([lindex $args 1])]
        set y [expr int([lindex $args 2])]
      } e]
      if { $err } {
        return -code error "apave::place: bad position: $e"
      }
      if {$::tcl_platform(platform) eq {windows}} {
        # handle windows multi-screen. -100 != +-100
        if {[string index [lindex $args 1] 0] ne {-}} {
          set x +$x
        }
        if {[string index [lindex $args 2] 0] ne {-}} {
          set y +$y
        }
      } else {
        if { $x >= 0 } {
          set x +$x
        }
        if { $y >= 0 } {
          set y +$y
        }
      }
    } else {
      if { $arglen == 2 } {
        set widget [lindex $args 1]
        if { ![winfo exists $widget] } {
          return -code error "apave::place: \"$widget\" does not exist"
        }
      } else {
        set widget .
      }
      set sw [winfo screenwidth  $path]
      set sh [winfo screenheight $path]
      if { $idx == 1 } {
        if { $arglen == 2 } {
          # center to widget
          set x0 [expr {[winfo rootx $widget] + ([winfo width  $widget] - $w)/2}]
          set y0 [expr {[winfo rooty $widget] + ([winfo height $widget] - $h)/2}]
        } else {
          # center to screen
          set x0 [expr {($sw - $w)/2 - [winfo vrootx $path]}]
          set y0 [expr {($sh - $h)/2 - [winfo vrooty $path]}]
        }
        set x +$x0
        set y +$y0
        if {$::tcl_platform(platform) ne {windows}} {
          if { $x0+$w > $sw } {set x {-0}; set x0 [expr {$sw-$w}]}
          if { $x0 < 0 }      {set x {+0}}
          if { $y0+$h > $sh } {set y {-0}; set y0 [expr {$sh-$h}]}
          if { $y0 < 0 }      {set y {+0}}
        }
      } else {
        set x0 [winfo rootx $widget]
        set y0 [winfo rooty $widget]
        set x1 [expr {$x0 + [winfo width  $widget]}]
        set y1 [expr {$y0 + [winfo height $widget]}]
        if { $idx == 2 || $idx == 3 } {
          set y +$y0
          if {$::tcl_platform(platform) ne {windows}} {
            if { $y0+$h > $sh } {set y {-0}; set y0 [expr {$sh-$h}]}
            if { $y0 < 0 }      {set y {+0}}
          }
          if { $idx == 2 } {
            # try left, then right if out, then 0 if out
            if { $x0 >= $w } {
              set x [expr {$x0-$w}]
            } elseif { $x1+$w <= $sw } {
              set x +$x1
            } else {
              set x {+0}
            }
          } else {
            # try right, then left if out, then 0 if out
            if { $x1+$w <= $sw } {
              set x +$x1
            } elseif { $x0 >= $w } {
              set x [expr {$x0-$w}]
            } else {
              set x {-0}
            }
          }
        } else {
          set x +$x0
          if {$::tcl_platform(platform) ne {windows}} {
            if { $x0+$w > $sw } {set x {-0}; set x0 [expr {$sw-$w}]}
            if { $x0 < 0 }      {set x {+0}}
          }
          if { $idx == 4 } {
            # try top, then bottom, then 0
            if { $h <= $y0 } {
              set y [expr {$y0-$h}]
            } elseif { $y1+$h <= $sh } {
              set y +$y1
            } else {
              set y {+0}
            }
          } else {
            # try bottom, then top, then 0
            if { $y1+$h <= $sh } {
              set y +$y1
            } elseif { $h <= $y0 } {
              set y [expr {$y0-$h}]
            } else {
              set y {-0}
            }
          }
        }
      }
    }

    ## If there's not a + or - in front of the number, we need to add one.
    if {[string is integer [string index $x 0]]} { set x +$x }
    if {[string is integer [string index $y 0]]} { set y +$y }

    wm geometry $path "${w}x${h}${x}${y}"
  } else {
    wm geometry $path "${w}x${h}"
  }
  update idletasks
}

## ________________________ EONS apave _________________________ ##

}

# ________________________ APave _________________________ #

oo::class create ::apave::APave {

superclass ::apave::APaveDialog

variable _savedvv

constructor {args} {
  # Creates APave object.
  #   win - window's name (path)
  #   args - additional arguments

  set _savedvv [list]
  if {[llength [self next]]} { next {*}$args }
}

destructor {
  # Clears variables used in the object.

  my initInput
  unset _savedvv
  if {[llength [self next]]} next
}
#_______________________

method initInput {} {
  # Initializes input and clears variables made in previous session.

  foreach {vn vv} $_savedvv {
    catch {unset $vn}
  }
  set _savedvv [list]
  set Widgetopts [list]
}
#_______________________

method varInput {} {
  # Gets variables made and filled in a previous session
  # as a list of "varname varvalue" pairs where varname
  # is of form: namespace::var$widgetname.

  return $_savedvv
}
#_______________________

method valueInput {} {
  # Gets input variables' values.

  set _values {}
  foreach {vnam -} [my varInput] {
    lappend _values [set $vnam]
  }
  return $_values
}
#_______________________

method input {icon ttl iopts args} {
  # Makes and runs an input dialog.
  #  icon - icon (omitted if equals to "")
  #  ttl - title of window
  #  iopts - list of widgets and their attributes
  #  args - list of dialog's attributes
  # The `iopts` contains lists of three items:
  #   name - name of widgets
  #   prompt - prompt for entering data
  #   valopts - value options
  # The `valopts` is a list specific for a widget's type, however
  # a first item of `valopts` is always an initial input value.

  if {$iopts ne {}} {
    my initInput  ;# clear away all internal vars
  }
  set pady "-pady 2"
  if {[set focusopt [::apave::getOption -focus {*}$args]] ne {}} {
    set focusopt "-focus $focusopt"
  }
  lappend inopts [list fraM + T 1 98 "-st nsew $pady -rw 1"]
  set savedvv [list]
  set frameprev {}
  foreach {name prompt valopts} $iopts {
    if {$name eq {}} continue
    lassign $prompt prompt gopts attrs
    lassign [::apave::extractOptions attrs -method {} -toprev {}] ismeth toprev
    if {[string toupper $name 0] eq $name} {
      set ismeth yes  ;# overcomes the above setting
      set name [string tolower $name 0]
    }
    set ismeth [string is true -strict $ismeth]
    set gopts "$pady $gopts"
    set typ [string tolower [string range $name 0 1]]
    if {$typ eq "v_" || $typ eq "se"} {
      lappend inopts [list fraM.$name - - - - "pack -fill x $gopts"]
      continue
    }
    set tvar "-tvar"
    switch -exact -- $typ {
      ch { set tvar "-var" }
      sp { set gopts "$gopts -expand 0 -side left"}
    }
    set framename fraM.fra$name
    if {$typ in {lb te tb}} {  ;# the widgets sized vertically
      lappend inopts [list $framename - - - - "pack -expand 1 -fill both"]
    } else {
      lappend inopts [list $framename - - - - "pack -fill x"]
    }
    set vv [my varName $name]
    set ff [my FieldName $name]
    set Name [string toupper $name 0]
    if {$ismeth && $typ ni {ra}} {
      # -method option forces making "WidgetName" method from "widgetName"
      my MakeWidgetName $ff $Name -
    }
    if {$typ ne {la} && $toprev eq {}} {
      set takfoc [::apave::parseOptions $attrs -takefocus 1]
      if {$focusopt eq {} && $takfoc} {
        if {$typ in {fi di cl fo da}} {
          set _ en*$name  ;# 'entry-like mega-widgets'
        } elseif {$typ eq "ft"} {
          set _ te*$name  ;# ftx - 'text-like mega-widget'
        } else {
          set _ $name
        }
        set focusopt "-focus $_"
      }
      if {$typ in {lb tb te}} {set anc nw} {set anc w}
      lappend inopts [list fraM.fra$name.labB$name - - - - \
        "pack -side left -anchor $anc -padx 3" \
        "-t \"$prompt\" -font \
        \"-family {[my basicTextFont]} -size [my basicFontSize]\""]
    }
    # for most widgets:
    #   1st item of 'valopts' list is the current value
    #   2nd and the rest of 'valopts' are a list of values
    if {$typ ni {fc te la}} {
      # curr.value can be set with a variable, so 'subst' is applied
      set vsel [lindex $valopts 0]
      catch {set vsel [subst -nocommands -nobackslashes $vsel]}
      set vlist [lrange $valopts 1 end]
    }
    if {[set msgLab [::apave::getOption -msgLab {*}$attrs]] ne {}} {
      set attrs [::apave::removeOptions $attrs -msgLab]
    }
    # define a current widget's info
    switch -exact -- $typ {
      lb - tb {
        set $vv $vlist
        lappend attrs -lvar $vv
        if {$vsel ni {{} -}} {
          lappend attrs -lbxsel "$::apave::UFF$vsel$::apave::UFF"
        }
        lappend inopts [list $ff - - - - \
          "pack -side left -expand 1 -fill both $gopts" $attrs]
        lappend inopts [list fraM.fra$name.sbv$name $ff L - - "pack -fill y"]
      }
      cb {
        if {![info exist $vv]} {catch {set $vv $vsel}}
        lappend attrs -tvar $vv -values $vlist
        if {$vsel ni {{} -}} {
          lappend attrs -cbxsel $::apave::UFF$vsel$::apave::UFF
        }
        lappend inopts [list $ff - - - - "pack -side left -expand 1 -fill x $gopts" $attrs]
      }
      fc {
        if {![info exist $vv]} {catch {set $vv {}}}
        lappend inopts [list $ff - - - - "pack -side left -expand 1 -fill x $gopts" "-tvar $vv -values \{$valopts\} $attrs"]
      }
      op {
        set $vv $vsel
        lappend inopts [list $ff - - - - "pack -fill x $gopts" "$vv $vlist"]
      }
      ra {
        if {![info exist $vv]} {catch {set $vv $vsel}}
        set padx 0
        foreach vo $vlist {
          set name $name
          set FF $ff[incr nnn]
          lappend inopts [list $FF - - - - "pack -side left $gopts -padx $padx" "-var $vv -value \"$vo\" -t \"$vo\" $attrs"]
          if {$ismeth} {
            my MakeWidgetName $FF $Name$nnn -
          }
          set padx [expr {$padx ? 0 : 9}]
        }
      }
      te {
        if {![info exist $vv]} {
          set valopts [string map [list \\n \n \\t \t] $valopts]
          set $vv [string map [list \\\\ \\ \\\} \} \\\{ \{] $valopts]
        }
        if {[dict exist $attrs -state] && [dict get $attrs -state] eq "disabled"} \
        {
          # disabled text widget cannot be filled with a text, so we should
          # compensate this through a home-made attribute (-disabledtext)
          set disattr "-disabledtext \{[set $vv]\}"
        } elseif {[dict exist $attrs -readonly] && [dict get $attrs -readonly] || [dict exist $attrs -ro] && [dict get $attrs -ro]} {
          set disattr "-rotext \{[set $vv]\}"
          set attrs [::apave::removeOptions $attrs -readonly -ro]
        } else {
          set disattr {}
        }
        lappend inopts [list $ff - - - - "pack -side left -expand 1 -fill both $gopts" "$attrs $disattr"]
        lappend inopts [list fraM.fra$name.sbv$name $ff L - - "pack -fill y"]
      }
      la {
        if {$prompt ne {}} { set prompt "-t \"$prompt\" " } ;# prompt as -text
        lappend inopts [list $ff - - - - "pack -anchor w $gopts" "$prompt$attrs"]
        continue
      }
      bu - bt - ch {
        set prompt {}
        if {$toprev eq {}} {
          lappend inopts [list $ff - - - - \
            "pack -side left -expand 1 -fill both $gopts" "$tvar $vv $attrs"]
        } else {
          lappend inopts [list $frameprev.$name - - - - \
            "pack -side left $gopts" "$tvar $vv $attrs"]
        }
        if {$vv ne {}} {
          if {![info exist $vv]} {
            catch {
              if {$vsel eq {}} {set vsel 0}
              set $vv $vsel
            }
          }
        }
      }
      default {
        if {$vlist ne {}} {lappend attrs -values $vlist}
        lappend inopts [list $ff - - - - \
          "pack -side left -expand 1 -fill x $gopts" "$tvar $vv $attrs"]
        if {$vv ne {}} {
          if {![info exist $vv]} {catch {set $vv $vsel}}
        }
      }
    }
    if {$msgLab ne {}} {
      lassign $msgLab lab msg attlab
      set lab [my parentWName [lindex $inopts end 0]].$lab
      if {$msg ne {}} {set msg "-t {$msg}"}
      append msg " $attlab"
      lappend inopts [list $lab - - - - "pack -side left -expand 1 -fill x" $msg]
    }
    if {![info exist $vv]} {set $vv {}}
    lappend _savedvv $vv [set $vv]
    set frameprev $framename
  }
  lassign [::apave::parseOptions $args -titleHELP {} -buttons {} -comOK 1 \
    -titleOK OK -titleCANCEL Cancel -centerme {}] \
    titleHELP buttons comOK titleOK titleCANCEL centerme
  if {$titleHELP eq {}} {
    set butHelp {}
  } else {
    lassign $titleHELP title command
    set butHelp [list butHELP $title $command]
  }
  if {$titleCANCEL eq {}} {
    set butCancel {}
  } else {
    set butCancel "butCANCEL $titleCANCEL destroy"
  }
  if {$centerme eq {}} {
    set centerme {-centerme 1}
  } else {
    set centerme "-centerme $centerme"
  }
  set args [::apave::removeOptions $args \
    -titleHELP -buttons -comOK -titleOK -titleCANCEL -centerme]
  lappend args {*}$focusopt
  if {[catch {
    lassign [my PrepArgs {*}$args] args
    set res [my Query $icon $ttl {} \
      "$butHelp $buttons butOK $titleOK $comOK $butCancel" \
      butOK $inopts $args {} {*}$centerme -input yes]} e]
  } then {
    catch {destroy $Dlgpath}  ;# Query's window
    set under \n[string repeat _ 80]\n\n
    ::apave::obj ok err "ERROR" "\n$e$under $inopts$under $args$under $centerme" \
      -t 1 -head "\nAPave error: \n" -hfg red -weight bold -w 80
    return 0
  }
  if {![lindex $res 0]} {  ;# restore old values if OK not chosen
    foreach {vn vv} $_savedvv {
      # tk_optionCascade (destroyed now) was tracing its variable => catch
      catch {set $vn $vv}
    }
  }
  return $res
}
#_______________________

method vieweditFile {fname {prepcom ""} args} {
  # Views or edits a file.
  #   fname - name of file
  #   prepcom - a command performing before and after creating a dialog
  #   args - additional options
  # It's a sort of stub for calling *editfile* method.
  # See also: editfile

  my editfile $fname {} {} {} $prepcom {*}$args
}
#_______________________

method editfile {fname fg bg cc {prepcom ""} args} {
  # Edits or views a file with a set of main colors
  #   fname - name of file
  #   fg - foreground color of text widget
  #   bg - background color of text widget
  #   cc - caret's color of text widget
  #   prepcom - a command performing before and after creating a dialog
  #   args - additional options (`-readonly 1` for viewing the file).
  # If *fg* isn't empty, all three colors are used to color a text.
  # See also:
  # [aplsimple.github.io](https://aplsimple.github.io/en/tcl/pave/index.html)

  if {$fname eq {}} {
    return false
  }
  set newfile 0
  if {[catch {set filetxt [::apave::readTextFile $fname {} yes]}]} {
    return false
  }
  lassign [::apave::parseOptions $args -rotext {} -readonly 1 -ro 1] rotext readonly ro
  lassign [::apave::extractOptions args -buttons {}] buttadd
  set btns {Close 0}  ;# by default 'view' mode
  set oper VIEW
  if {$rotext eq {} && (!$readonly || !$ro)} {
    set btns {Save 1 Close 0}
    set oper EDIT
  }
  if {$fg eq {}} {
    set tclr {}
  } else {
    set tclr "-fg $fg -bg $bg -cc $cc"
  }
  if {$prepcom eq {}} {set aa {}} {set aa [$prepcom filetxt]}
  set res [my misc {} "$oper: $fname" "$filetxt" "$buttadd $btns" \
    TEXT -text 1 -w {100 80} -h 32 {*}$tclr \
    -post $prepcom {*}$aa {*}$args]
  set data [string range $res 2 end]
  if {[set res [string index $res 0]] eq "1"} {
    set data [string range $data [string first " " $data]+1 end]
    set data [string trimright $data]
    set res [::apave::writeTextFile $fname data]
  } elseif {$newfile} {
    file delete $fname
  }
  return $res
}
#_______________________

method onTop {wpar top {wtoplist -} {res ""}} {
  # Sets -topmost attribute for windows or gets a list of topmost windows.
  #   wpar - parent window's path
  #   top - -topmost attribute's value
  #   wtoplist - list of windows to process
  #   res - used to get the result
  # Returns a list of "topmost=$top" windows found on $wpar path.

  set res {}
  if {$wtoplist ne "-"} {
    # sets the attribute
    foreach w $wtoplist {wm attributes $w -topmost $top}
  } else {
    # gets a list of topmost windows
    if {$wpar ne {}} {
      catch {
        set res [my onTop [winfo parent $wpar] $top - $res]
        if {[wm attributes $wpar -topmost]==$top} {lappend res $wpar}
      }
    }
  }
  return $res
}

# ________________________ EONS _________________________ #

}
# ________________________ EOF _________________________ #

apave.tcl