| 1 | # git-gui status bar mega-widget |
| 2 | # Copyright (C) 2007 Shawn Pearce |
| 3 | |
| 4 | # The status_bar class manages the entire status bar. It is possible for |
| 5 | # multiple overlapping asynchronous operations to want to display status |
| 6 | # simultaneously. Each one receives a status_bar_operation when it calls the |
| 7 | # start method, and the status bar combines all active operations into the |
| 8 | # line of text it displays. Most of the time, there will be at most one |
| 9 | # ongoing operation. |
| 10 | # |
| 11 | # Note that the entire status bar can be either in single-line or two-line |
| 12 | # mode, depending on the constructor. Multiple active operations are only |
| 13 | # supported for single-line status bars. |
| 14 | |
| 15 | class status_bar { |
| 16 | |
| 17 | field allow_multiple ; # configured at construction |
| 18 | |
| 19 | field w ; # our own window path |
| 20 | field w_l ; # text widget we draw messages into |
| 21 | field w_c ; # canvas we draw a progress bar into |
| 22 | field c_pack ; # script to pack the canvas with |
| 23 | |
| 24 | field baseline_text ; # text to show if there are no operations |
| 25 | field status_bar_text ; # combined text for all operations |
| 26 | |
| 27 | field operations ; # list of current ongoing operations |
| 28 | |
| 29 | # The status bar can display a progress bar, updated when consumers call the |
| 30 | # update method on their status_bar_operation. When there are multiple |
| 31 | # operations, the status bar shows the combined status of all operations. |
| 32 | # |
| 33 | # When an overlapping operation completes, the progress bar is going to |
| 34 | # abruptly have one fewer operation in the calculation, causing a discontinuity. |
| 35 | # Therefore, whenever an operation completes, if it is not the last operation, |
| 36 | # this counter is increased, and the progress bar is calculated as though there |
| 37 | # were still another operation at 100%. When the last operation completes, this |
| 38 | # is reset to 0. |
| 39 | field completed_operation_count |
| 40 | |
| 41 | constructor new {path} { |
| 42 | set w $path |
| 43 | set w_l $w.l |
| 44 | set w_c $w.c |
| 45 | |
| 46 | # Standard single-line status bar: Permit overlapping operations |
| 47 | set allow_multiple 1 |
| 48 | |
| 49 | set baseline_text "" |
| 50 | set operations [list] |
| 51 | set completed_operation_count 0 |
| 52 | |
| 53 | ttk::frame $w |
| 54 | ttk::label $w_l \ |
| 55 | -textvariable @status_bar_text \ |
| 56 | -anchor w \ |
| 57 | -justify left |
| 58 | pack $w_l -side left |
| 59 | set c_pack [cb _oneline_pack] |
| 60 | |
| 61 | bind $w <Destroy> [cb _delete %W] |
| 62 | return $this |
| 63 | } |
| 64 | |
| 65 | method _oneline_pack {} { |
| 66 | $w_c conf -width 100 |
| 67 | pack $w_c -side right |
| 68 | } |
| 69 | |
| 70 | constructor two_line {path} { |
| 71 | set w $path |
| 72 | set w_l $w.l |
| 73 | set w_c $w.c |
| 74 | |
| 75 | # Two-line status bar: Only one ongoing operation permitted. |
| 76 | set allow_multiple 0 |
| 77 | |
| 78 | set baseline_text "" |
| 79 | set operations [list] |
| 80 | set completed_operation_count 0 |
| 81 | |
| 82 | ttk::frame $w |
| 83 | ttk::label $w_l \ |
| 84 | -textvariable @status_bar_text \ |
| 85 | -anchor w \ |
| 86 | -justify left |
| 87 | pack $w_l -anchor w -fill x |
| 88 | set c_pack [list pack $w_c -fill x] |
| 89 | |
| 90 | bind $w <Destroy> [cb _delete %W] |
| 91 | return $this |
| 92 | } |
| 93 | |
| 94 | method ensure_canvas {} { |
| 95 | if {[winfo exists $w_c]} { |
| 96 | $w_c coords bar 0 0 0 20 |
| 97 | } else { |
| 98 | canvas $w_c \ |
| 99 | -height [expr {int([winfo reqheight $w_l] * 0.6)}] \ |
| 100 | -borderwidth 1 \ |
| 101 | -relief groove \ |
| 102 | -highlightt 0 |
| 103 | $w_c create rectangle 0 0 0 20 -tags bar -fill navy |
| 104 | eval $c_pack |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | method show {msg} { |
| 109 | $this ensure_canvas |
| 110 | set baseline_text $msg |
| 111 | $this refresh |
| 112 | } |
| 113 | |
| 114 | method start {msg {uds {}}} { |
| 115 | set baseline_text "" |
| 116 | |
| 117 | if {!$allow_multiple && [llength $operations]} { |
| 118 | return [lindex $operations 0] |
| 119 | } |
| 120 | |
| 121 | $this ensure_canvas |
| 122 | |
| 123 | set operation [status_bar_operation::new $this $msg $uds] |
| 124 | |
| 125 | lappend operations $operation |
| 126 | |
| 127 | $this refresh |
| 128 | |
| 129 | return $operation |
| 130 | } |
| 131 | |
| 132 | method refresh {} { |
| 133 | set new_text "" |
| 134 | |
| 135 | set total [expr $completed_operation_count * 100] |
| 136 | set have $total |
| 137 | |
| 138 | foreach operation $operations { |
| 139 | if {$new_text != ""} { |
| 140 | append new_text " / " |
| 141 | } |
| 142 | |
| 143 | append new_text [$operation get_status] |
| 144 | |
| 145 | set total [expr $total + 100] |
| 146 | set have [expr $have + [$operation get_progress]] |
| 147 | } |
| 148 | |
| 149 | if {$new_text == ""} { |
| 150 | set new_text $baseline_text |
| 151 | } |
| 152 | |
| 153 | set status_bar_text $new_text |
| 154 | |
| 155 | if {[winfo exists $w_c]} { |
| 156 | set pixel_width 0 |
| 157 | if {$have > 0} { |
| 158 | set pixel_width [expr {[winfo width $w_c] * $have / $total}] |
| 159 | } |
| 160 | |
| 161 | $w_c coords bar 0 0 $pixel_width 20 |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | method stop {operation stop_msg} { |
| 166 | set idx [lsearch $operations $operation] |
| 167 | |
| 168 | if {$idx >= 0} { |
| 169 | set operations [lreplace $operations $idx $idx] |
| 170 | set completed_operation_count [expr \ |
| 171 | $completed_operation_count + 1] |
| 172 | |
| 173 | if {[llength $operations] == 0} { |
| 174 | set completed_operation_count 0 |
| 175 | |
| 176 | destroy $w_c |
| 177 | if {$stop_msg ne {}} { |
| 178 | set baseline_text $stop_msg |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | $this refresh |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | method stop_all {{stop_msg {}}} { |
| 187 | # This makes the operation's call to stop a no-op. |
| 188 | set operations_copy $operations |
| 189 | set operations [list] |
| 190 | |
| 191 | foreach operation $operations_copy { |
| 192 | $operation stop |
| 193 | } |
| 194 | |
| 195 | if {$stop_msg ne {}} { |
| 196 | set baseline_text $stop_msg |
| 197 | } |
| 198 | |
| 199 | $this refresh |
| 200 | } |
| 201 | |
| 202 | method _delete {current} { |
| 203 | if {$current eq $w} { |
| 204 | delete_this |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | } |
| 209 | |
| 210 | # The status_bar_operation class tracks a single consumer's ongoing status bar |
| 211 | # activity, with the context that there are a few situations where multiple |
| 212 | # overlapping asynchronous operations might want to display status information |
| 213 | # simultaneously. Instances of status_bar_operation are created by calling |
| 214 | # start on the status_bar, and when the caller is done with its stauts bar |
| 215 | # operation, it calls stop on the operation. |
| 216 | |
| 217 | class status_bar_operation { |
| 218 | |
| 219 | field status_bar; # reference back to the status_bar that owns this object |
| 220 | |
| 221 | field is_active; |
| 222 | |
| 223 | field status {}; # single line of text we show |
| 224 | field progress {}; # current progress (0 to 100) |
| 225 | field prefix {}; # text we format into status |
| 226 | field units {}; # unit of progress |
| 227 | field meter {}; # current core git progress meter (if active) |
| 228 | |
| 229 | constructor new {owner msg uds} { |
| 230 | set status_bar $owner |
| 231 | |
| 232 | set status $msg |
| 233 | set progress 0 |
| 234 | set prefix $msg |
| 235 | set units $uds |
| 236 | set meter {} |
| 237 | |
| 238 | set is_active 1 |
| 239 | |
| 240 | return $this |
| 241 | } |
| 242 | |
| 243 | method get_is_active {} { return $is_active } |
| 244 | method get_status {} { return $status } |
| 245 | method get_progress {} { return $progress } |
| 246 | |
| 247 | method update {have total} { |
| 248 | if {!$is_active} { return } |
| 249 | |
| 250 | set progress 0 |
| 251 | |
| 252 | if {$total > 0} { |
| 253 | set progress [expr {100 * $have / $total}] |
| 254 | } |
| 255 | |
| 256 | set prec [string length [format %i $total]] |
| 257 | |
| 258 | set status [mc "%s ... %*i of %*i %s (%3i%%)" \ |
| 259 | $prefix \ |
| 260 | $prec $have \ |
| 261 | $prec $total \ |
| 262 | $units $progress] |
| 263 | |
| 264 | $status_bar refresh |
| 265 | } |
| 266 | |
| 267 | method update_meter {buf} { |
| 268 | if {!$is_active} { return } |
| 269 | |
| 270 | append meter $buf |
| 271 | set r [string last "\r" $meter] |
| 272 | if {$r == -1} { |
| 273 | return |
| 274 | } |
| 275 | |
| 276 | set prior [string range $meter 0 $r] |
| 277 | set meter [string range $meter [expr {$r + 1}] end] |
| 278 | set p "\\((\\d+)/(\\d+)\\)" |
| 279 | if {[regexp ":\\s*\\d+% $p\(?:, done.\\s*\n|\\s*\r)\$" $prior _j a b]} { |
| 280 | update $this $a $b |
| 281 | } elseif {[regexp "$p\\s+done\r\$" $prior _j a b]} { |
| 282 | update $this $a $b |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | method stop {{stop_msg {}}} { |
| 287 | if {$is_active} { |
| 288 | set is_active 0 |
| 289 | $status_bar stop $this $stop_msg |
| 290 | } |
| 291 | } |
| 292 | |
| 293 | method restart {msg} { |
| 294 | if {!$is_active} { return } |
| 295 | |
| 296 | set status $msg |
| 297 | set prefix $msg |
| 298 | set meter {} |
| 299 | $status_bar refresh |
| 300 | } |
| 301 | |
| 302 | method _delete {} { |
| 303 | stop |
| 304 | delete_this |
| 305 | } |
| 306 | |
| 307 | } |