Statistics
| Branch: | Tag: | Revision:

mininet / examples / miniedit.py @ 28f46c8d

History | View | Annotate | Download (25 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
    # Callback ignores event
303
    # pylint: disable-msg=W0613
304
    def deleteSelection( self, event ):
305
        "Delete the selected item."
306
        if self.selection is not None:
307
            self.deleteItem( self.selection )
308
        self.selectItem( None )
309
    # pylint: enable-msg=W0613
310

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

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

    
335
    def clickHost( self, event ):
336
        "Add a new host to our canvas."
337
        self.newNode( 'Host', event )
338

    
339
    def clickSwitch( self, event ):
340
        "Add a new switch to our canvas."
341
        self.newNode( 'Switch', event )
342

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

    
353
    # Callback ignores event
354
    # pylint: disable-msg=W0613
355
    def releaseLink( self, event ):
356
        "Give up on the current link."
357
        if self.link is not None:
358
            self.canvas.delete( self.link )
359
        self.linkWidget = self.linkItem = self.link = None
360
    # pylint: enable-msg=W0613
361

    
362
    # Generic node handlers
363

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

    
379
    def selectItem( self, item ):
380
        "Select an item and remember old selection."
381
        self.lastSelection = self.selection
382
        self.selection = item
383

    
384
    def enterNode( self, event ):
385
        "Select node on entry."
386
        self.selectNode( event )
387

    
388
    # Callback ignores event
389
    # pylint: disable-msg=W0613
390
    def leaveNode( self, event ):
391
        "Restore old selection on exit."
392
        self.selectItem( self.lastSelection )
393
    # pylint: enable-msg=W0613
394

    
395
    def clickNode( self, event ):
396
        "Node click handler."
397
        if self.active is 'Link':
398
            self.startLink( event )
399
        else:
400
            self.selectNode( event )
401
        return 'break'
402

    
403
    def dragNode( self, event ):
404
        "Node drag handler."
405
        if self.active is 'Link':
406
            self.dragLink( event )
407
        else:
408
            self.dragNodeAround( event )
409

    
410
    def releaseNode( self, event ):
411
        "Node release handler."
412
        if self.active is 'Link':
413
            self.finishLink( event )
414

    
415
    # Specific node handlers
416

    
417
    def selectNode( self, event ):
418
        "Select the node that was clicked on."
419
        item = self.widgetToItem.get( event.widget, None )
420
        self.selectItem( item )
421

    
422
    def dragNodeAround( self, event ):
423
        "Drag a node around on the canvas."
424
        c = self.canvas
425
        # Convert global to local coordinates;
426
        # Necessary since x, y are widget-relative
427
        x = self.canvasx( event.x_root )
428
        y = self.canvasy( event.y_root )
429
        w = event.widget
430
        # Adjust node position
431
        item = self.widgetToItem[ w ]
432
        c.coords( item, x, y )
433
        # Adjust link positions
434
        for dest in w.links:
435
            link = w.links[ dest ]
436
            item = self.widgetToItem[ dest ]
437
            x1, y1 = c.coords( item )
438
            c.coords( link, x, y, x1, y1 )
439

    
440
    def startLink( self, event ):
441
        "Start a new link."
442
        if event.widget not in self.widgetToItem:
443
            # Didn't click on a node
444
            return
445
        w = event.widget
446
        item = self.widgetToItem[ w ]
447
        x, y = self.canvas.coords( item )
448
        self.link = self.canvas.create_line( x, y, x, y, width=4,
449
            fill='blue', tag='link' )
450
        self.linkx, self.linky = x, y
451
        self.linkWidget = w
452
        self.linkItem = item
453

    
454
        # Link bindings
455
        # Selection still needs a bit of work overall
456
        # Callbacks ignore event
457
        # pylint: disable-msg=W0613
458

    
459
        def select( event, link=self.link ):
460
            "Select item on mouse entry."
461
            self.selectItem( link )
462

    
463
        def highlight( event, link=self.link ):
464
            "Highlight item on mouse entry."
465
            # self.selectItem( link )
466
            self.canvas.itemconfig( link, fill='green' )
467

    
468
        def unhighlight( event, link=self.link ):
469
            "Unhighlight item on mouse exit."
470
            self.canvas.itemconfig( link, fill='blue' )
471
            # self.selectItem( None )
472

    
473
        # pylint: disable-msg=W0613
474
        self.canvas.tag_bind( self.link, '<Enter>', highlight )
475
        self.canvas.tag_bind( self.link, '<Leave>', unhighlight )
476
        self.canvas.tag_bind( self.link, '<ButtonPress-1>', select )
477

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

    
504
    # Menu handlers
505

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

    
529
    def createToolImages( self ):
530
        "Create toolbar (and icon) images."
531

    
532
    # Model interface
533
    #
534
    # Ultimately we will either want to use a topo or
535
    # mininet object here, probably.
536

    
537
    def addLink( self, source, dest ):
538
        "Add link to model."
539
        source.links[ dest ] = self.link
540
        dest.links[ source ] = self.link
541
        self.links[ self.link ] = ( source, dest )
542

    
543
    def deleteLink( self, link ):
544
        "Delete link from model."
545
        pair = self.links.get( link, None )
546
        if pair is not None:
547
            source, dest = pair
548
            del source.links[ dest ]
549
            del dest.links[ source ]
550
        if link is not None:
551
            del self.links[ link ]
552

    
553
    def deleteNode( self, item ):
554
        "Delete node (and its links) from model."
555
        widget = self.itemToWidget[ item ]
556
        for link in widget.links.values():
557
            # Delete from view and model
558
            self.deleteItem( link )
559
        del self.itemToWidget[ item ]
560
        del self.widgetToItem[ widget ]
561

    
562
    def build( self ):
563
        "Build network based on our topology."
564

    
565
        net = Mininet( topo=None )
566

    
567
        # Make controller
568
        net.addController( 'c0' )
569
        # Make nodes
570
        for widget in self.widgetToItem:
571
            name = widget[ 'text' ]
572
            tags = self.canvas.gettags( self.widgetToItem[ widget ] )
573
            nodeNum = int( name[ 1: ] )
574
            if 'Switch' in tags:
575
                net.addSwitch( name )
576
            elif 'Host' in tags:
577
                net.addHost( name, ip=ipStr( nodeNum ) )
578
            else:
579
                raise Exception( "Cannot create mystery node: " + name )
580
        # Make links
581
        for link in self.links.values():
582
            ( src, dst ) = link
583
            srcName, dstName = src[ 'text' ], dst[ 'text' ]
584
            src, dst = net.nameToNode[ srcName ], net.nameToNode[ dstName ]
585
            src.linkTo( dst )
586

    
587
        # Build network (we have to do this separately at the moment )
588
        net.build()
589

    
590
        return net
591

    
592
    def start( self ):
593
        "Start network."
594
        if self.net is None:
595
            self.net = self.build()
596
            self.net.start()
597

    
598
    def stop( self ):
599
        "Stop network."
600
        if self.net is not None:
601
            self.net.stop()
602
        cleanUpScreens()
603
        self.net = None
604

    
605
    def xterm( self, _ignore=None ):
606
        "Make an xterm when a button is pressed."
607
        if ( self.selection is None or
608
             self.net is None or
609
             self.selection not in self.itemToWidget ):
610
            return
611
        name = self.itemToWidget[ self.selection ][ 'text' ]
612
        if name not in self.net.nameToNode:
613
            return
614
        term = makeTerm( self.net.nameToNode[ name ], 'Host' )
615
        self.net.terms.append( term )
616

    
617

    
618
def miniEditImages():
619
    "Create and return images for MiniEdit."
620

    
621
    # Image data. Git will be unhappy. However, the alternative
622
    # is to keep track of separate binary files, which is also
623
    # unappealing.
624

    
625
    return {
626
        'Select': BitmapImage(
627
            file='/usr/include/X11/bitmaps/left_ptr' ),
628

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

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

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

    
711
if __name__ == '__main__':
712
    setLogLevel( 'info' )
713
    app = MiniEdit()
714
    app.mainloop()