# git-gui status bar mega-widget
# Copyright (C) 2007 Shawn Pearce
# The status_bar class manages the entire status bar. It is possible for
# multiple overlapping asynchronous operations to want to display status
# simultaneously. Each one receives a status_bar_operation when it calls the
# start method, and the status bar combines all active operations into the
# line of text it displays. Most of the time, there will be at most one
# ongoing operation.
#
# Note that the entire status bar can be either in single-line or two-line
# mode, depending on the constructor. Multiple active operations are only
# supported for single-line status bars.
class status_bar {
field allow_multiple ; # configured at construction
field w ; # our own window path
field w_l ; # text widget we draw messages into
field w_c ; # canvas we draw a progress bar into
field c_pack ; # script to pack the canvas with
field baseline_text ; # text to show if there are no operations
field status_bar_text ; # combined text for all operations
field operations ; # list of current ongoing operations
# The status bar can display a progress bar, updated when consumers call the
# update method on their status_bar_operation. When there are multiple
# operations, the status bar shows the combined status of all operations.
#
# When an overlapping operation completes, the progress bar is going to
# abruptly have one fewer operation in the calculation, causing a discontinuity.
# Therefore, whenever an operation completes, if it is not the last operation,
# this counter is increased, and the progress bar is calculated as though there
# were still another operation at 100%. When the last operation completes, this
# is reset to 0.
field completed_operation_count
constructor new {path} {
global use_ttk NS
set w $path
set w_l $w.l
set w_c $w.c
# Standard single-line status bar: Permit overlapping operations
set allow_multiple 1
set baseline_text ""
set operations [list]
set completed_operation_count 0
${NS}::frame $w
if {!$use_ttk} {
$w configure -borderwidth 1 -relief sunken
}
${NS}::label $w_l \
-textvariable @status_bar_text \
-anchor w \
-justify left
pack $w_l -side left
set c_pack [cb _oneline_pack]
bind $w <Destroy> [cb _delete %W]
return $this
}
method _oneline_pack {} {
$w_c conf -width 100
pack $w_c -side right
}
constructor two_line {path} {
global NS
set w $path
set w_l $w.l
set w_c $w.c
# Two-line status bar: Only one ongoing operation permitted.
set allow_multiple 0
set baseline_text ""
set operations [list]
set completed_operation_count 0
${NS}::frame $w
${NS}::label $w_l \
-textvariable @status_bar_text \
-anchor w \
-justify left
pack $w_l -anchor w -fill x
set c_pack [list pack $w_c -fill x]
bind $w <Destroy> [cb _delete %W]
return $this
}
method ensure_canvas {} {
if {[winfo exists $w_c]} {
$w_c coords bar 0 0 0 20
} else {
canvas $w_c \
-height [expr {int([winfo reqheight $w_l] * 0.6)}] \
-borderwidth 1 \
-relief groove \
-highlightt 0
$w_c create rectangle 0 0 0 20 -tags bar -fill navy
eval $c_pack
}
}
method show {msg} {
$this ensure_canvas
set baseline_text $msg
$this refresh
}
method start {msg {uds {}}} {
set baseline_text ""
if {!$allow_multiple && [llength $operations]} {
return [lindex $operations 0]
}
$this ensure_canvas
set operation [status_bar_operation::new $this $msg $uds]
lappend operations $operation
$this refresh
return $operation
}
method refresh {} {
set new_text ""
set total [expr $completed_operation_count * 100]
set have $total
foreach operation $operations {
if {$new_text != ""} {
append new_text " / "
}
append new_text [$operation get_status]
set total [expr $total + 100]
set have [expr $have + [$operation get_progress]]
}
if {$new_text == ""} {
set new_text $baseline_text
}
set status_bar_text $new_text
if {[winfo exists $w_c]} {
set pixel_width 0
if {$have > 0} {
set pixel_width [expr {[winfo width $w_c] * $have / $total}]
}
$w_c coords bar 0 0 $pixel_width 20
}
}
method stop {operation stop_msg} {
set idx [lsearch $operations $operation]
if {$idx >= 0} {
set operations [lreplace $operations $idx $idx]
set completed_operation_count [expr \
$completed_operation_count + 1]
if {[llength $operations] == 0} {
set completed_operation_count 0
destroy $w_c
if {$stop_msg ne {}} {
set baseline_text $stop_msg
}
}
$this refresh
}
}
method stop_all {{stop_msg {}}} {
# This makes the operation's call to stop a no-op.
set operations_copy $operations
set operations [list]
foreach operation $operations_copy {
$operation stop
}
if {$stop_msg ne {}} {
set baseline_text $stop_msg
}
$this refresh
}
method _delete {current} {
if {$current eq $w} {
delete_this
}
}
}
# The status_bar_operation class tracks a single consumer's ongoing status bar
# activity, with the context that there are a few situations where multiple
# overlapping asynchronous operations might want to display status information
# simultaneously. Instances of status_bar_operation are created by calling
# start on the status_bar, and when the caller is done with its stauts bar
# operation, it calls stop on the operation.
class status_bar_operation {
field status_bar; # reference back to the status_bar that owns this object
field is_active;
field status {}; # single line of text we show
field progress {}; # current progress (0 to 100)
field prefix {}; # text we format into status
field units {}; # unit of progress
field meter {}; # current core git progress meter (if active)
constructor new {owner msg uds} {
set status_bar $owner
set status $msg
set progress 0
set prefix $msg
set units $uds
set meter {}
set is_active 1
return $this
}
method get_is_active {} { return $is_active }
method get_status {} { return $status }
method get_progress {} { return $progress }
method update {have total} {
if {!$is_active} { return }
set progress 0
if {$total > 0} {
set progress [expr {100 * $have / $total}]
}
set prec [string length [format %i $total]]
set status [mc "%s ... %*i of %*i %s (%3i%%)" \
$prefix \
$prec $have \
$prec $total \
$units $progress]
$status_bar refresh
}
method update_meter {buf} {
if {!$is_active} { return }
append meter $buf
set r [string last "\r" $meter]
if {$r == -1} {
return
}
set prior [string range $meter 0 $r]
set meter [string range $meter [expr {$r + 1}] end]
set p "\\((\\d+)/(\\d+)\\)"
if {[regexp ":\\s*\\d+% $p\(?:, done.\\s*\n|\\s*\r)\$" $prior _j a b]} {
update $this $a $b
} elseif {[regexp "$p\\s+done\r\$" $prior _j a b]} {
update $this $a $b
}
}
method stop {{stop_msg {}}} {
if {$is_active} {
set is_active 0
$status_bar stop $this $stop_msg
}
}
method restart {msg} {
if {!$is_active} { return }
set status $msg
set prefix $msg
set meter {}
$status_bar refresh
}
method _delete {} {
stop
delete_this
}
}