Statistics
| Branch: | Tag: | Revision:

mininet / examples / miniedit.py @ edf60032

History | View | Annotate | Download (24.8 KB)

1
#!/usr/bin/python
2

    
3
"""
4
MiniEdit: a simple network editor for Mininet
5

6
This is a simple demonstration of how one might build a
7
GUI application using Mininet as the network model.
8

9
Development version - not entirely functional!
10

11
Bob Lantz, April 2010
12
"""
13

    
14
from Tkinter import Frame, Button, Label, Scrollbar, Canvas
15
from Tkinter import Menu, BitmapImage, PhotoImage, Wm, Toplevel
16

    
17
# someday: from ttk import *
18

    
19
from mininet.log import setLogLevel
20
from mininet.net import Mininet
21
from mininet.util import ipStr
22
from mininet.term import makeTerm, cleanUpScreens
23

    
24
class MiniEdit( Frame ):
25

    
26
    "A simple network editor for Mininet."
27

    
28
    def __init__( self, parent=None, cheight=200, cwidth=500 ):
29

    
30
        Frame.__init__( self, parent )
31
        self.action = None
32
        self.appName = 'MiniEdit'
33

    
34
        # Style
35
        self.font = ( 'Geneva', 9 )
36
        self.smallFont = ( 'Geneva', 7 )
37
        self.bg = 'white'
38

    
39
        # Title
40
        self.top = self.winfo_toplevel()
41
        self.top.title( self.appName )
42

    
43
        # Menu bar
44
        self.createMenubar()
45

    
46
        # Editing canvas
47
        self.cheight, self.cwidth = cheight, cwidth
48
        self.cframe, self.canvas = self.createCanvas()
49

    
50
        # Toolbar
51
        self.images = miniEditImages()
52
        self.buttons = {}
53
        self.active = None
54
        self.tools = ( 'Select', 'Host', 'Switch', 'Link' )
55
        self.customColors = { 'Switch': 'darkGreen', 'Host': 'blue' }
56
        self.toolbar = self.createToolbar()
57

    
58
        # Layout
59
        self.toolbar.grid( column=0, row=0, sticky='nsew')
60
        self.cframe.grid( column=1, row=0 )
61
        self.columnconfigure( 1, weight=1 )
62
        self.rowconfigure( 0, weight=1 )
63
        self.pack( expand=True, fill='both' )
64

    
65
        # About box
66
        self.aboutBox = None
67

    
68
        # Initialize node data
69
        self.nodeBindings = self.createNodeBindings()
70
        self.nodePrefixes = { 'Switch': 's', 'Host': 'h' }
71
        self.widgetToItem = {}
72
        self.itemToWidget = {}
73

    
74
        # Initialize link tool
75
        self.link = self.linkWidget = None
76

    
77
        # Selection support
78
        self.selection = None
79

    
80
        # Keyboard bindings
81
        self.bind( '<Control-q>', lambda event: self.quit() )
82
        self.bind( '<KeyPress-Delete>', self.deleteSelection )
83
        self.bind( '<KeyPress-BackSpace>', self.deleteSelection )
84
        self.focus()
85

    
86
        # Event handling initalization
87
        self.linkx = self.linky = self.linkItem = None
88
        self.lastSelection = None
89

    
90
        # Model initialization
91
        self.links = {}
92
        self.nodeCount = 0
93
        self.net = None
94

    
95
        # Close window gracefully
96
        Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit )
97

    
98
    def quit( self ):
99
        "Stop our network, if any, then quit."
100
        self.stop()
101
        Frame.quit( self )
102

    
103
    def createMenubar( self ):
104
        "Create our menu bar."
105

    
106
        font = self.font
107

    
108
        mbar = Menu( self.top, font=font )
109
        self.top.configure( menu=mbar )
110

    
111
        # Application menu
112
        appMenu = Menu( mbar, tearoff=False )
113
        mbar.add_cascade( label=self.appName, font=font, menu=appMenu )
114
        appMenu.add_command( label='About MiniEdit', command=self.about,
115
                             font=font)
116
        appMenu.add_separator()
117
        appMenu.add_command( label='Quit', command=self.quit, font=font )
118

    
119
        #fileMenu = Menu( mbar, tearoff=False )
120
        #mbar.add_cascade( label="File", font=font, menu=fileMenu )
121
        #fileMenu.add_command( label="Load...", font=font )
122
        #fileMenu.add_separator()
123
        #fileMenu.add_command( label="Save", font=font )
124
        #fileMenu.add_separator()
125
        #fileMenu.add_command( label="Print", font=font )
126

    
127
        editMenu = Menu( mbar, tearoff=False )
128
        mbar.add_cascade( label="Edit", font=font, menu=editMenu )
129
        editMenu.add_command( label="Cut", font=font,
130
                              command=lambda: self.deleteSelection( None ) )
131

    
132
        runMenu = Menu( mbar, tearoff=False )
133
        mbar.add_cascade( label="Run", font=font, menu=runMenu )
134
        runMenu.add_command( label="Run", font=font, command=self.doRun )
135
        runMenu.add_command( label="Stop", font=font, command=self.doStop )
136
        runMenu.add_separator()
137
        runMenu.add_command( label='Xterm', font=font, command=self.xterm )
138

    
139
    # Canvas
140

    
141
    def createCanvas( self ):
142
        "Create and return our scrolling canvas frame."
143
        f = Frame( self )
144

    
145
        canvas = Canvas( f, width=self.cwidth, height=self.cheight,
146
                         bg=self.bg )
147

    
148
        # Scroll bars
149
        xbar = Scrollbar( f, orient='horizontal', command=canvas.xview )
150
        ybar = Scrollbar( f, orient='vertical', command=canvas.yview )
151
        canvas.configure( xscrollcommand=xbar.set, yscrollcommand=ybar.set )
152

    
153
        # Resize box
154
        resize = Label( f, bg='white' )
155

    
156
        # Layout
157
        canvas.grid( row=0, column=1, sticky='nsew')
158
        ybar.grid( row=0, column=2, sticky='ns')
159
        xbar.grid( row=1, column=1, sticky='ew' )
160
        resize.grid( row=1, column=2, sticky='nsew' )
161

    
162
        # Resize behavior
163
        f.rowconfigure( 0, weight=1 )
164
        f.columnconfigure( 1, weight=1 )
165
        f.grid( row=0, column=0, sticky='nsew' )
166
        f.bind( '<Configure>', lambda event: self.updateScrollRegion() )
167

    
168
        # Mouse bindings
169
        canvas.bind( '<ButtonPress-1>', self.clickCanvas )
170
        canvas.bind( '<B1-Motion>', self.dragCanvas )
171
        canvas.bind( '<ButtonRelease-1>', self.releaseCanvas )
172

    
173
        return f, canvas
174

    
175
    def updateScrollRegion( self ):
176
        "Update canvas scroll region to hold everything."
177
        bbox = self.canvas.bbox( 'all' )
178
        if bbox is not None:
179
            self.canvas.configure( scrollregion=( 0, 0, bbox[ 2 ],
180
                                   bbox[ 3 ] ) )
181

    
182
    def canvasx( self, x_root ):
183
        "Convert root x coordinate to canvas coordinate."
184
        c = self.canvas
185
        return c.canvasx( x_root ) - c.winfo_rootx()
186

    
187
    def canvasy( self, y_root ):
188
        "Convert root y coordinate to canvas coordinate."
189
        c = self.canvas
190
        return c.canvasy( y_root ) - c.winfo_rooty()
191

    
192
    # Toolbar
193

    
194
    def activate( self, toolName ):
195
        "Activate a tool and press its button."
196
        # Adjust button appearance
197
        if self.active:
198
            self.buttons[ self.active ].configure( relief='raised' )
199
        self.buttons[ toolName ].configure( relief='sunken' )
200
        # Activate dynamic bindings
201
        self.active = toolName
202

    
203
    def createToolbar( self ):
204
        "Create and return our toolbar frame."
205

    
206
        toolbar = Frame( self )
207

    
208
        # Tools
209
        for tool in self.tools:
210
            cmd = ( lambda t=tool: self.activate( t ) )
211
            b = Button( toolbar, text=tool, font=self.smallFont, command=cmd)
212
            if tool in self.images:
213
                b.config( height=35, image=self.images[ tool ] )
214
                # b.config( compound='top' )
215
            b.pack( fill='x' )
216
            self.buttons[ tool ] = b
217
        self.activate( self.tools[ 0 ] )
218

    
219
        # Spacer
220
        Label( toolbar, text='' ).pack()
221

    
222
        # Commands
223
        for cmd, color in [ ( 'Stop', 'darkRed' ), ( 'Run', 'darkGreen' ) ]:
224
            doCmd = getattr( self, 'do' + cmd )
225
            b = Button( toolbar, text=cmd, font=self.smallFont,
226
                        fg=color, command=doCmd )
227
            b.pack( fill='x', side='bottom' )
228

    
229
        return toolbar
230

    
231
    def doRun( self ):
232
        "Run command."
233
        self.activate( 'Select' )
234
        for tool in self.tools:
235
            self.buttons[ tool ].config( state='disabled' )
236
        self.start()
237

    
238
    def doStop( self ):
239
        "Stop command."
240
        self.stop()
241
        for tool in self.tools:
242
            self.buttons[ tool ].config( state='normal' )
243

    
244
    # Generic canvas handler
245
    #
246
    # We could have used bindtags, as in nodeIcon, but
247
    # the dynamic approach used here
248
    # may actually require less code. In any case, it's an
249
    # interesting introspection-based alternative to bindtags.
250

    
251
    def canvasHandle( self, eventName, event ):
252
        "Generic canvas event handler"
253
        if self.active is None:
254
            return
255
        toolName = self.active
256
        handler = getattr( self, eventName + toolName, None )
257
        if handler is not None:
258
            handler( event )
259

    
260
    def clickCanvas( self, event ):
261
        "Canvas click handler."
262
        self.canvasHandle( 'click', event )
263

    
264
    def dragCanvas( self, event ):
265
        "Canvas drag handler."
266
        self.canvasHandle( 'drag', event )
267

    
268
    def releaseCanvas( self, event ):
269
        "Canvas mouse up handler."
270
        self.canvasHandle( 'release', event )
271

    
272
    # Currently the only items we can select directly are
273
    # links. Nodes are handled by bindings in the node icon.
274

    
275
    def findItem( self, x, y ):
276
        "Find items at a location in our canvas."
277
        items = self.canvas.find_overlapping( x, y, x, y )
278
        if len( items ) == 0:
279
            return None
280
        else:
281
            return items[ 0 ]
282

    
283
    # Canvas bindings for Select, Host, Switch and Link tools
284

    
285
    def clickSelect( self, event ):
286
        "Select an item."
287
        self.selectItem( self.findItem( event.x, event.y ) )
288

    
289
    def deleteItem( self, item ):
290
        "Delete an item."
291
        # Don't delete while network is running
292
        if self.buttons[ 'Select' ][ 'state' ] == 'disabled':
293
            return
294
        # Delete from model
295
        if item in self.links:
296
            self.deleteLink( item )
297
        if item in self.itemToWidget:
298
            self.deleteNode( item )
299
        # Delete from view
300
        self.canvas.delete( item )
301

    
302
    def deleteSelection( self, _event ):
303
        "Delete the selected item."
304
        if self.selection is not None:
305
            self.deleteItem( self.selection )
306
        self.selectItem( None )
307

    
308
    def nodeIcon( self, node, name ):
309
        "Create a new node icon."
310
        icon = Button( self.canvas, image=self.images[ node ],
311
                       text=name, compound='top' )
312
        # Unfortunately bindtags wants a tuple
313
        bindtags = [ str( self.nodeBindings ) ]
314
        bindtags += list( icon.bindtags() )
315
        icon.bindtags( tuple( bindtags ) )
316
        return icon
317

    
318
    def newNode( self, node, event ):
319
        "Add a new node to our canvas."
320
        c = self.canvas
321
        x, y = c.canvasx( event.x ), c.canvasy( event.y )
322
        self.nodeCount += 1
323
        name = self.nodePrefixes[ node ] + str( self.nodeCount )
324
        icon = self.nodeIcon( node, name )
325
        item = self.canvas.create_window( x, y, anchor='c', window=icon,
326
                                          tags=node )
327
        self.widgetToItem[ icon ] = item
328
        self.itemToWidget[ item ] = icon
329
        self.selectItem( item )
330
        icon.links = {}
331

    
332
    def clickHost( self, event ):
333
        "Add a new host to our canvas."
334
        self.newNode( 'Host', event )
335

    
336
    def clickSwitch( self, event ):
337
        "Add a new switch to our canvas."
338
        self.newNode( 'Switch', event )
339

    
340
    def dragLink( self, event ):
341
        "Drag a link's endpoint to another node."
342
        if self.link is None:
343
            return
344
        # Since drag starts in widget, we use root coords
345
        x = self.canvasx( event.x_root )
346
        y = self.canvasy( event.y_root )
347
        c = self.canvas
348
        c.coords( self.link, self.linkx, self.linky, x, y )
349

    
350
    def releaseLink( self, _event ):
351
        "Give up on the current link."
352
        if self.link is not None:
353
            self.canvas.delete( self.link )
354
        self.linkWidget = self.linkItem = self.link = None
355

    
356
    # Generic node handlers
357

    
358
    def createNodeBindings( self ):
359
        "Create a set of bindings for nodes."
360
        bindings = {
361
            '<ButtonPress-1>': self.clickNode,
362
            '<B1-Motion>': self.dragNode,
363
            '<ButtonRelease-1>': self.releaseNode,
364
            '<Enter>': self.enterNode,
365
            '<Leave>': self.leaveNode,
366
            '<Double-ButtonPress-1>': self.xterm
367
        }
368
        l = Label()  # lightweight-ish owner for bindings
369
        for event, binding in bindings.items():
370
            l.bind( event, binding )
371
        return l
372

    
373
    def selectItem( self, item ):
374
        "Select an item and remember old selection."
375
        self.lastSelection = self.selection
376
        self.selection = item
377

    
378
    def enterNode( self, event ):
379
        "Select node on entry."
380
        self.selectNode( event )
381

    
382
    def leaveNode( self, _event ):
383
        "Restore old selection on exit."
384
        self.selectItem( self.lastSelection )
385

    
386
    def clickNode( self, event ):
387
        "Node click handler."
388
        if self.active is 'Link':
389
            self.startLink( event )
390
        else:
391
            self.selectNode( event )
392
        return 'break'
393

    
394
    def dragNode( self, event ):
395
        "Node drag handler."
396
        if self.active is 'Link':
397
            self.dragLink( event )
398
        else:
399
            self.dragNodeAround( event )
400

    
401
    def releaseNode( self, event ):
402
        "Node release handler."
403
        if self.active is 'Link':
404
            self.finishLink( event )
405

    
406
    # Specific node handlers
407

    
408
    def selectNode( self, event ):
409
        "Select the node that was clicked on."
410
        item = self.widgetToItem.get( event.widget, None )
411
        self.selectItem( item )
412

    
413
    def dragNodeAround( self, event ):
414
        "Drag a node around on the canvas."
415
        c = self.canvas
416
        # Convert global to local coordinates;
417
        # Necessary since x, y are widget-relative
418
        x = self.canvasx( event.x_root )
419
        y = self.canvasy( event.y_root )
420
        w = event.widget
421
        # Adjust node position
422
        item = self.widgetToItem[ w ]
423
        c.coords( item, x, y )
424
        # Adjust link positions
425
        for dest in w.links:
426
            link = w.links[ dest ]
427
            item = self.widgetToItem[ dest ]
428
            x1, y1 = c.coords( item )
429
            c.coords( link, x, y, x1, y1 )
430

    
431
    def startLink( self, event ):
432
        "Start a new link."
433
        if event.widget not in self.widgetToItem:
434
            # Didn't click on a node
435
            return
436
        w = event.widget
437
        item = self.widgetToItem[ w ]
438
        x, y = self.canvas.coords( item )
439
        self.link = self.canvas.create_line( x, y, x, y, width=4,
440
                                             fill='blue', tag='link' )
441
        self.linkx, self.linky = x, y
442
        self.linkWidget = w
443
        self.linkItem = item
444

    
445
        # Link bindings
446
        # Selection still needs a bit of work overall
447
        # Callbacks ignore event
448

    
449
        def select( _event, link=self.link ):
450
            "Select item on mouse entry."
451
            self.selectItem( link )
452

    
453
        def highlight( _event, link=self.link ):
454
            "Highlight item on mouse entry."
455
            # self.selectItem( link )
456
            self.canvas.itemconfig( link, fill='green' )
457

    
458
        def unhighlight( _event, link=self.link ):
459
            "Unhighlight item on mouse exit."
460
            self.canvas.itemconfig( link, fill='blue' )
461
            # self.selectItem( None )
462

    
463
        self.canvas.tag_bind( self.link, '<Enter>', highlight )
464
        self.canvas.tag_bind( self.link, '<Leave>', unhighlight )
465
        self.canvas.tag_bind( self.link, '<ButtonPress-1>', select )
466

    
467
    def finishLink( self, event ):
468
        "Finish creating a link"
469
        if self.link is None:
470
            return
471
        source = self.linkWidget
472
        c = self.canvas
473
        # Since we dragged from the widget, use root coords
474
        x, y = self.canvasx( event.x_root ), self.canvasy( event.y_root )
475
        target = self.findItem( x, y )
476
        dest = self.itemToWidget.get( target, None )
477
        if ( source is None or dest is None or source == dest
478
                or dest in source.links or source in dest.links ):
479
            self.releaseLink( event )
480
            return
481
        # For now, don't allow hosts to be directly linked
482
        stags = self.canvas.gettags( self.widgetToItem[ source ] )
483
        dtags = self.canvas.gettags( target )
484
        if 'Host' in stags and 'Host' in dtags:
485
            self.releaseLink( event )
486
            return
487
        x, y = c.coords( target )
488
        c.coords( self.link, self.linkx, self.linky, x, y )
489
        self.addLink( source, dest )
490
        # We're done
491
        self.link = self.linkWidget = None
492

    
493
    # Menu handlers
494

    
495
    def about( self ):
496
        "Display about box."
497
        about = self.aboutBox
498
        if about is None:
499
            bg = 'white'
500
            about = Toplevel( bg='white' )
501
            about.title( 'About' )
502
            info = self.appName + ': a simple network editor for MiniNet'
503
            warning = 'Development version - not entirely functional!'
504
            author = 'Bob Lantz <rlantz@cs>, April 2010'
505
            line1 = Label( about, text=info, font='Helvetica 10 bold', bg=bg )
506
            line2 = Label( about, text=warning, font='Helvetica 9', bg=bg )
507
            line3 = Label( about, text=author, font='Helvetica 9', bg=bg )
508
            line1.pack( padx=20, pady=10 )
509
            line2.pack(pady=10 )
510
            line3.pack(pady=10 )
511
            hide = ( lambda about=about: about.withdraw() )
512
            self.aboutBox = about
513
            # Hide on close rather than destroying window
514
            Wm.wm_protocol( about, name='WM_DELETE_WINDOW', func=hide )
515
        # Show (existing) window
516
        about.deiconify()
517

    
518
    def createToolImages( self ):
519
        "Create toolbar (and icon) images."
520

    
521
    # Model interface
522
    #
523
    # Ultimately we will either want to use a topo or
524
    # mininet object here, probably.
525

    
526
    def addLink( self, source, dest ):
527
        "Add link to model."
528
        source.links[ dest ] = self.link
529
        dest.links[ source ] = self.link
530
        self.links[ self.link ] = ( source, dest )
531

    
532
    def deleteLink( self, link ):
533
        "Delete link from model."
534
        pair = self.links.get( link, None )
535
        if pair is not None:
536
            source, dest = pair
537
            del source.links[ dest ]
538
            del dest.links[ source ]
539
        if link is not None:
540
            del self.links[ link ]
541

    
542
    def deleteNode( self, item ):
543
        "Delete node (and its links) from model."
544
        widget = self.itemToWidget[ item ]
545
        for link in widget.links.values():
546
            # Delete from view and model
547
            self.deleteItem( link )
548
        del self.itemToWidget[ item ]
549
        del self.widgetToItem[ widget ]
550

    
551
    def build( self ):
552
        "Build network based on our topology."
553

    
554
        net = Mininet( topo=None )
555

    
556
        # Make controller
557
        net.addController( 'c0' )
558
        # Make nodes
559
        for widget in self.widgetToItem:
560
            name = widget[ 'text' ]
561
            tags = self.canvas.gettags( self.widgetToItem[ widget ] )
562
            nodeNum = int( name[ 1: ] )
563
            if 'Switch' in tags:
564
                net.addSwitch( name )
565
            elif 'Host' in tags:
566
                net.addHost( name, ip=ipStr( nodeNum ) )
567
            else:
568
                raise Exception( "Cannot create mystery node: " + name )
569
        # Make links
570
        for link in self.links.values():
571
            ( src, dst ) = link
572
            srcName, dstName = src[ 'text' ], dst[ 'text' ]
573
            src, dst = net.nameToNode[ srcName ], net.nameToNode[ dstName ]
574
            src.linkTo( dst )
575

    
576
        # Build network (we have to do this separately at the moment )
577
        net.build()
578

    
579
        return net
580

    
581
    def start( self ):
582
        "Start network."
583
        if self.net is None:
584
            self.net = self.build()
585
            self.net.start()
586

    
587
    def stop( self ):
588
        "Stop network."
589
        if self.net is not None:
590
            self.net.stop()
591
        cleanUpScreens()
592
        self.net = None
593

    
594
    def xterm( self, _=None ):
595
        "Make an xterm when a button is pressed."
596
        if ( self.selection is None or
597
             self.net is None or
598
             self.selection not in self.itemToWidget ):
599
            return
600
        name = self.itemToWidget[ self.selection ][ 'text' ]
601
        if name not in self.net.nameToNode:
602
            return
603
        term = makeTerm( self.net.nameToNode[ name ], 'Host' )
604
        self.net.terms.append( term )
605

    
606

    
607
def miniEditImages():
608
    "Create and return images for MiniEdit."
609

    
610
    # Image data. Git will be unhappy. However, the alternative
611
    # is to keep track of separate binary files, which is also
612
    # unappealing.
613

    
614
    return {
615
        'Select': BitmapImage(
616
            file='/usr/include/X11/bitmaps/left_ptr' ),
617

    
618
        'Host': PhotoImage( data=r"""
619
            R0lGODlhIAAYAPcAMf//////zP//mf//Zv//M///AP/M///MzP/M
620
            mf/MZv/MM//MAP+Z//+ZzP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9m
621
            Zv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8AZv8A
622
            M/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zM
623
            AMyZ/8yZzMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz
624
            /8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wAAJn//5n/
625
            zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZ
626
            mZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkz
627
            ZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/
628
            M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZ
629
            AGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA
630
            /2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPM
631
            zDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNm
632
            mTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMA
633
            ZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDM
634
            MwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBm
635
            AAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0A
636
            ALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI
637
            AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA
638
            RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAA
639
            ACH5BAEAAAAALAAAAAAgABgAAAiNAAH8G0iwoMGDCAcKTMiw4UBw
640
            BPXVm0ixosWLFvVBHFjPoUeC9Tb+6/jRY0iQ/8iVbHiS40CVKxG2
641
            HEkQZsyCM0mmvGkw50uePUV2tEnOZkyfQA8iTYpTKNOgKJ+C3AhO
642
            p9SWVaVOfWj1KdauTL9q5UgVbFKsEjGqXVtP40NwcBnCjXtw7tx/
643
            C8cSBBAQADs=
644
        """ ),
645

    
646
        'Switch': PhotoImage( data=r"""
647
            R0lGODlhIAAYAPcAMf//////zP//mf//Zv//M///AP/M///MzP/M
648
            mf/MZv/MM//MAP+Z//+ZzP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9m
649
            Zv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8AZv8A
650
            M/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zM
651
            AMyZ/8yZzMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz
652
            /8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wAAJn//5n/
653
            zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZ
654
            mZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkz
655
            ZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/
656
            M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZ
657
            AGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA
658
            /2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPM
659
            zDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNm
660
            mTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMA
661
            ZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDM
662
            MwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBm
663
            AAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0A
664
            ALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI
665
            AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA
666
            RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAA
667
            ACH5BAEAAAAALAAAAAAgABgAAAhwAAEIHEiwoMGDCBMqXMiwocOH
668
            ECNKnEixosWB3zJq3Mixo0eNAL7xG0mypMmTKPl9Cznyn8uWL/m5
669
            /AeTpsyYI1eKlBnO5r+eLYHy9Ck0J8ubPmPOrMmUpM6UUKMa/Ui1
670
            6saLWLNq3cq1q9evYB0GBAA7
671
        """ ),
672

    
673
        'Link': PhotoImage( data=r"""
674
            R0lGODlhFgAWAPcAMf//////zP//mf//Zv//M///AP/M///MzP/M
675
            mf/MZv/MM//MAP+Z//+ZzP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9m
676
            Zv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8AZv8A
677
            M/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zM
678
            AMyZ/8yZzMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz
679
            /8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wAAJn//5n/
680
            zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZ
681
            mZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkz
682
            ZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/
683
            M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZ
684
            AGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA
685
            /2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPM
686
            zDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNm
687
            mTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMA
688
            ZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDM
689
            MwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBm
690
            AAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0A
691
            ALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI
692
            AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA
693
            RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAA
694
            ACH5BAEAAAAALAAAAAAWABYAAAhIAAEIHEiwoEGBrhIeXEgwoUKG
695
            Cx0+hGhQoiuKBy1irChxY0GNHgeCDAlgZEiTHlFuVImRJUWXEGEy
696
            lBmxI8mSNknm1Dnx5sCAADs=
697
        """ )
698
    }
699

    
700
if __name__ == '__main__':
701
    setLogLevel( 'info' )
702
    app = MiniEdit()
703
    app.mainloop()