| 1 | #!/usr/bin/python |
|---|
| 2 | # |
|---|
| 3 | # Urwid graphics example program |
|---|
| 4 | # Copyright (C) 2004-2011 Ian Ward |
|---|
| 5 | # |
|---|
| 6 | # This library is free software; you can redistribute it and/or |
|---|
| 7 | # modify it under the terms of the GNU Lesser General Public |
|---|
| 8 | # License as published by the Free Software Foundation; either |
|---|
| 9 | # version 2.1 of the License, or (at your option) any later version. |
|---|
| 10 | # |
|---|
| 11 | # This library is distributed in the hope that it will be useful, |
|---|
| 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|---|
| 14 | # Lesser General Public License for more details. |
|---|
| 15 | # |
|---|
| 16 | # You should have received a copy of the GNU Lesser General Public |
|---|
| 17 | # License along with this library; if not, write to the Free Software |
|---|
| 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|---|
| 19 | # |
|---|
| 20 | # Urwid web site: http://excess.org/urwid/ |
|---|
| 21 | |
|---|
| 22 | """ |
|---|
| 23 | Urwid example demonstrating use of the BarGraph widget and creating a |
|---|
| 24 | floating-window appearance. Also shows use of alarms to create timed |
|---|
| 25 | animation. |
|---|
| 26 | """ |
|---|
| 27 | |
|---|
| 28 | import urwid |
|---|
| 29 | |
|---|
| 30 | import math |
|---|
| 31 | import time |
|---|
| 32 | |
|---|
| 33 | UPDATE_INTERVAL = 0.2 |
|---|
| 34 | |
|---|
| 35 | def sin100( x ): |
|---|
| 36 | """ |
|---|
| 37 | A sin function that returns values between 0 and 100 and repeats |
|---|
| 38 | after x == 100. |
|---|
| 39 | """ |
|---|
| 40 | return 50 + 50 * math.sin( x * math.pi / 50 ) |
|---|
| 41 | |
|---|
| 42 | class GraphModel: |
|---|
| 43 | """ |
|---|
| 44 | A class responsible for storing the data that will be displayed |
|---|
| 45 | on the graph, and keeping track of which mode is enabled. |
|---|
| 46 | """ |
|---|
| 47 | |
|---|
| 48 | data_max_value = 100 |
|---|
| 49 | |
|---|
| 50 | def __init__(self): |
|---|
| 51 | data = [ ('Saw', range(0,100,2)*2), |
|---|
| 52 | ('Square', [0]*30 + [100]*30), |
|---|
| 53 | ('Sine 1', [sin100(x) for x in range(100)] ), |
|---|
| 54 | ('Sine 2', [(sin100(x) + sin100(x*2))/2 |
|---|
| 55 | for x in range(100)] ), |
|---|
| 56 | ('Sine 3', [(sin100(x) + sin100(x*3))/2 |
|---|
| 57 | for x in range(100)] ), |
|---|
| 58 | ] |
|---|
| 59 | self.modes = [] |
|---|
| 60 | self.data = {} |
|---|
| 61 | for m, d in data: |
|---|
| 62 | self.modes.append(m) |
|---|
| 63 | self.data[m] = d |
|---|
| 64 | |
|---|
| 65 | def get_modes(self): |
|---|
| 66 | return self.modes |
|---|
| 67 | |
|---|
| 68 | def set_mode(self, m): |
|---|
| 69 | self.current_mode = m |
|---|
| 70 | |
|---|
| 71 | def get_data(self, offset, r): |
|---|
| 72 | """ |
|---|
| 73 | Return the data in [offset:offset+r], the maximum value |
|---|
| 74 | for items returned, and the offset at which the data |
|---|
| 75 | repeats. |
|---|
| 76 | """ |
|---|
| 77 | l = [] |
|---|
| 78 | d = self.data[self.current_mode] |
|---|
| 79 | while r: |
|---|
| 80 | offset = offset % len(d) |
|---|
| 81 | segment = d[offset:offset+r] |
|---|
| 82 | r -= len(segment) |
|---|
| 83 | offset += len(segment) |
|---|
| 84 | l += segment |
|---|
| 85 | return l, self.data_max_value, len(d) |
|---|
| 86 | |
|---|
| 87 | |
|---|
| 88 | class GraphView(urwid.WidgetWrap): |
|---|
| 89 | """ |
|---|
| 90 | A class responsible for providing the application's interface and |
|---|
| 91 | graph display. |
|---|
| 92 | """ |
|---|
| 93 | palette = [ |
|---|
| 94 | ('body', 'black', 'light gray', 'standout'), |
|---|
| 95 | ('header', 'white', 'dark red', 'bold'), |
|---|
| 96 | ('screen edge', 'light blue', 'dark cyan'), |
|---|
| 97 | ('main shadow', 'dark gray', 'black'), |
|---|
| 98 | ('line', 'black', 'light gray', 'standout'), |
|---|
| 99 | ('bg background','light gray', 'black'), |
|---|
| 100 | ('bg 1', 'black', 'dark blue', 'standout'), |
|---|
| 101 | ('bg 1 smooth', 'dark blue', 'black'), |
|---|
| 102 | ('bg 2', 'black', 'dark cyan', 'standout'), |
|---|
| 103 | ('bg 2 smooth', 'dark cyan', 'black'), |
|---|
| 104 | ('button normal','light gray', 'dark blue', 'standout'), |
|---|
| 105 | ('button select','white', 'dark green'), |
|---|
| 106 | ('line', 'black', 'light gray', 'standout'), |
|---|
| 107 | ('pg normal', 'white', 'black', 'standout'), |
|---|
| 108 | ('pg complete', 'white', 'dark magenta'), |
|---|
| 109 | ('pg smooth', 'dark magenta','black') |
|---|
| 110 | ] |
|---|
| 111 | |
|---|
| 112 | graph_samples_per_bar = 10 |
|---|
| 113 | graph_num_bars = 5 |
|---|
| 114 | graph_offset_per_second = 5 |
|---|
| 115 | |
|---|
| 116 | def __init__(self, controller): |
|---|
| 117 | self.controller = controller |
|---|
| 118 | self.started = True |
|---|
| 119 | self.start_time = None |
|---|
| 120 | self.offset = 0 |
|---|
| 121 | self.last_offset = None |
|---|
| 122 | urwid.WidgetWrap.__init__(self, self.main_window()) |
|---|
| 123 | |
|---|
| 124 | def get_offset_now(self): |
|---|
| 125 | if self.start_time is None: |
|---|
| 126 | return 0 |
|---|
| 127 | if not self.started: |
|---|
| 128 | return self.offset |
|---|
| 129 | tdelta = time.time() - self.start_time |
|---|
| 130 | return int(self.offset + (tdelta*self.graph_offset_per_second)) |
|---|
| 131 | |
|---|
| 132 | def update_graph(self, force_update=False): |
|---|
| 133 | o = self.get_offset_now() |
|---|
| 134 | if o == self.last_offset and not force_update: |
|---|
| 135 | return False |
|---|
| 136 | self.last_offset = o |
|---|
| 137 | gspb = self.graph_samples_per_bar |
|---|
| 138 | r = gspb * self.graph_num_bars |
|---|
| 139 | d, max_value, repeat = self.controller.get_data( o, r ) |
|---|
| 140 | l = [] |
|---|
| 141 | for n in range(self.graph_num_bars): |
|---|
| 142 | value = sum(d[n*gspb:(n+1)*gspb])/gspb |
|---|
| 143 | # toggle between two bar types |
|---|
| 144 | if n & 1: |
|---|
| 145 | l.append([0,value]) |
|---|
| 146 | else: |
|---|
| 147 | l.append([value,0]) |
|---|
| 148 | self.graph.set_data(l,max_value) |
|---|
| 149 | |
|---|
| 150 | # also update progress |
|---|
| 151 | if (o//repeat)&1: |
|---|
| 152 | # show 100% for first half, 0 for second half |
|---|
| 153 | if o%repeat > repeat//2: |
|---|
| 154 | prog = 0 |
|---|
| 155 | else: |
|---|
| 156 | prog = 1 |
|---|
| 157 | else: |
|---|
| 158 | prog = float(o%repeat) / repeat |
|---|
| 159 | self.animate_progress.set_completion( prog ) |
|---|
| 160 | return True |
|---|
| 161 | |
|---|
| 162 | def on_animate_button(self, button): |
|---|
| 163 | """Toggle started state and button text.""" |
|---|
| 164 | if self.started: # stop animation |
|---|
| 165 | button.set_label("Start") |
|---|
| 166 | self.offset = self.get_offset_now() |
|---|
| 167 | self.started = False |
|---|
| 168 | self.controller.stop_animation() |
|---|
| 169 | else: |
|---|
| 170 | button.set_label("Stop") |
|---|
| 171 | self.started = True |
|---|
| 172 | self.start_time = time.time() |
|---|
| 173 | self.controller.animate_graph() |
|---|
| 174 | |
|---|
| 175 | |
|---|
| 176 | def on_reset_button(self, w): |
|---|
| 177 | self.offset = 0 |
|---|
| 178 | self.start_time = time.time() |
|---|
| 179 | self.update_graph(True) |
|---|
| 180 | |
|---|
| 181 | def on_mode_button(self, button, state): |
|---|
| 182 | """Notify the controller of a new mode setting.""" |
|---|
| 183 | if state: |
|---|
| 184 | # The new mode is the label of the button |
|---|
| 185 | self.controller.set_mode( button.get_label() ) |
|---|
| 186 | self.last_offset = None |
|---|
| 187 | |
|---|
| 188 | def on_mode_change(self, m): |
|---|
| 189 | """Handle external mode change by updating radio buttons.""" |
|---|
| 190 | for rb in self.mode_buttons: |
|---|
| 191 | if rb.get_label() == m: |
|---|
| 192 | rb.set_state(True, do_callback=False) |
|---|
| 193 | break |
|---|
| 194 | self.last_offset = None |
|---|
| 195 | |
|---|
| 196 | def on_unicode_checkbox(self, w, state): |
|---|
| 197 | self.graph = self.bar_graph( state ) |
|---|
| 198 | self.graph_wrap._w = self.graph |
|---|
| 199 | self.animate_progress = self.progress_bar( state ) |
|---|
| 200 | self.animate_progress_wrap._w = self.animate_progress |
|---|
| 201 | self.update_graph( True ) |
|---|
| 202 | |
|---|
| 203 | |
|---|
| 204 | def main_shadow(self, w): |
|---|
| 205 | """Wrap a shadow and background around widget w.""" |
|---|
| 206 | bg = urwid.AttrWrap(urwid.SolidFill(u"\u2592"), 'screen edge') |
|---|
| 207 | shadow = urwid.AttrWrap(urwid.SolidFill(u" "), 'main shadow') |
|---|
| 208 | |
|---|
| 209 | bg = urwid.Overlay( shadow, bg, |
|---|
| 210 | ('fixed left', 3), ('fixed right', 1), |
|---|
| 211 | ('fixed top', 2), ('fixed bottom', 1)) |
|---|
| 212 | w = urwid.Overlay( w, bg, |
|---|
| 213 | ('fixed left', 2), ('fixed right', 3), |
|---|
| 214 | ('fixed top', 1), ('fixed bottom', 2)) |
|---|
| 215 | return w |
|---|
| 216 | |
|---|
| 217 | def bar_graph(self, smooth=False): |
|---|
| 218 | satt = None |
|---|
| 219 | if smooth: |
|---|
| 220 | satt = {(1,0): 'bg 1 smooth', (2,0): 'bg 2 smooth'} |
|---|
| 221 | w = urwid.BarGraph(['bg background','bg 1','bg 2'], satt=satt) |
|---|
| 222 | return w |
|---|
| 223 | |
|---|
| 224 | def button(self, t, fn): |
|---|
| 225 | w = urwid.Button(t, fn) |
|---|
| 226 | w = urwid.AttrWrap(w, 'button normal', 'button select') |
|---|
| 227 | return w |
|---|
| 228 | |
|---|
| 229 | def radio_button(self, g, l, fn): |
|---|
| 230 | w = urwid.RadioButton(g, l, False, on_state_change=fn) |
|---|
| 231 | w = urwid.AttrWrap(w, 'button normal', 'button select') |
|---|
| 232 | return w |
|---|
| 233 | |
|---|
| 234 | def progress_bar(self, smooth=False): |
|---|
| 235 | if smooth: |
|---|
| 236 | return urwid.ProgressBar('pg normal', 'pg complete', |
|---|
| 237 | 0, 1, 'pg smooth') |
|---|
| 238 | else: |
|---|
| 239 | return urwid.ProgressBar('pg normal', 'pg complete', |
|---|
| 240 | 0, 1) |
|---|
| 241 | |
|---|
| 242 | def exit_program(self, w): |
|---|
| 243 | raise urwid.ExitMainLoop() |
|---|
| 244 | |
|---|
| 245 | def graph_controls(self): |
|---|
| 246 | modes = self.controller.get_modes() |
|---|
| 247 | # setup mode radio buttons |
|---|
| 248 | self.mode_buttons = [] |
|---|
| 249 | group = [] |
|---|
| 250 | for m in modes: |
|---|
| 251 | rb = self.radio_button( group, m, self.on_mode_button ) |
|---|
| 252 | self.mode_buttons.append( rb ) |
|---|
| 253 | # setup animate button |
|---|
| 254 | self.animate_button = self.button( "", self.on_animate_button) |
|---|
| 255 | self.on_animate_button( self.animate_button ) |
|---|
| 256 | self.offset = 0 |
|---|
| 257 | self.animate_progress = self.progress_bar() |
|---|
| 258 | animate_controls = urwid.GridFlow( [ |
|---|
| 259 | self.animate_button, |
|---|
| 260 | self.button("Reset", self.on_reset_button), |
|---|
| 261 | ], 9, 2, 0, 'center') |
|---|
| 262 | |
|---|
| 263 | if urwid.get_encoding_mode() == "utf8": |
|---|
| 264 | unicode_checkbox = urwid.CheckBox( |
|---|
| 265 | "Enable Unicode Graphics", |
|---|
| 266 | on_state_change=self.on_unicode_checkbox) |
|---|
| 267 | else: |
|---|
| 268 | unicode_checkbox = urwid.Text( |
|---|
| 269 | "UTF-8 encoding not detected") |
|---|
| 270 | |
|---|
| 271 | self.animate_progress_wrap = urwid.WidgetWrap( |
|---|
| 272 | self.animate_progress) |
|---|
| 273 | |
|---|
| 274 | l = [ urwid.Text("Mode",align="center"), |
|---|
| 275 | ] + self.mode_buttons + [ |
|---|
| 276 | urwid.Divider(), |
|---|
| 277 | urwid.Text("Animation",align="center"), |
|---|
| 278 | animate_controls, |
|---|
| 279 | self.animate_progress_wrap, |
|---|
| 280 | urwid.Divider(), |
|---|
| 281 | urwid.LineBox( unicode_checkbox ), |
|---|
| 282 | urwid.Divider(), |
|---|
| 283 | self.button("Quit", self.exit_program ), |
|---|
| 284 | ] |
|---|
| 285 | w = urwid.ListBox(urwid.SimpleListWalker(l)) |
|---|
| 286 | return w |
|---|
| 287 | |
|---|
| 288 | def main_window(self): |
|---|
| 289 | self.graph = self.bar_graph() |
|---|
| 290 | self.graph_wrap = urwid.WidgetWrap( self.graph ) |
|---|
| 291 | vline = urwid.AttrWrap( urwid.SolidFill(u'\u2502'), 'line') |
|---|
| 292 | c = self.graph_controls() |
|---|
| 293 | w = urwid.Columns([('weight',2,self.graph_wrap), |
|---|
| 294 | ('fixed',1,vline), c], |
|---|
| 295 | dividechars=1, focus_column=2) |
|---|
| 296 | w = urwid.Padding(w,('fixed left',1),('fixed right',0)) |
|---|
| 297 | w = urwid.AttrWrap(w,'body') |
|---|
| 298 | w = urwid.LineBox(w) |
|---|
| 299 | w = urwid.AttrWrap(w,'line') |
|---|
| 300 | w = self.main_shadow(w) |
|---|
| 301 | return w |
|---|
| 302 | |
|---|
| 303 | |
|---|
| 304 | class GraphController: |
|---|
| 305 | """ |
|---|
| 306 | A class responsible for setting up the model and view and running |
|---|
| 307 | the application. |
|---|
| 308 | """ |
|---|
| 309 | def __init__(self): |
|---|
| 310 | self.animate_alarm = None |
|---|
| 311 | self.model = GraphModel() |
|---|
| 312 | self.view = GraphView( self ) |
|---|
| 313 | # use the first mode as the default |
|---|
| 314 | mode = self.get_modes()[0] |
|---|
| 315 | self.model.set_mode( mode ) |
|---|
| 316 | # update the view |
|---|
| 317 | self.view.on_mode_change( mode ) |
|---|
| 318 | self.view.update_graph(True) |
|---|
| 319 | |
|---|
| 320 | def get_modes(self): |
|---|
| 321 | """Allow our view access to the list of modes.""" |
|---|
| 322 | return self.model.get_modes() |
|---|
| 323 | |
|---|
| 324 | def set_mode(self, m): |
|---|
| 325 | """Allow our view to set the mode.""" |
|---|
| 326 | rval = self.model.set_mode( m ) |
|---|
| 327 | self.view.update_graph(True) |
|---|
| 328 | return rval |
|---|
| 329 | |
|---|
| 330 | def get_data(self, offset, range): |
|---|
| 331 | """Provide data to our view for the graph.""" |
|---|
| 332 | return self.model.get_data( offset, range ) |
|---|
| 333 | |
|---|
| 334 | |
|---|
| 335 | def main(self): |
|---|
| 336 | self.loop = urwid.MainLoop(self.view, self.view.palette) |
|---|
| 337 | self.loop.run() |
|---|
| 338 | |
|---|
| 339 | def animate_graph(self, loop=None, user_data=None): |
|---|
| 340 | """update the graph and schedule the next update""" |
|---|
| 341 | self.view.update_graph() |
|---|
| 342 | self.animate_alarm = self.loop.set_alarm_in( |
|---|
| 343 | UPDATE_INTERVAL, self.animate_graph) |
|---|
| 344 | |
|---|
| 345 | def stop_animation(self): |
|---|
| 346 | """stop animating the graph""" |
|---|
| 347 | if self.animate_alarm: |
|---|
| 348 | self.loop.remove_alarm(self.animate_alarm) |
|---|
| 349 | self.animate_alarm = None |
|---|
| 350 | |
|---|
| 351 | |
|---|
| 352 | def main(): |
|---|
| 353 | GraphController().main() |
|---|
| 354 | |
|---|
| 355 | if '__main__'==__name__: |
|---|
| 356 | main() |
|---|