Wiki

root/cobra/trunk/Source/ObjectExplorer-WinForms.cobra

Revision 2151, 20.2 KB (checked in by Chuck.Esterbrook, 3 years ago)

Minor tweaks.

  • Property svn:eol-style set to native
Line 
1"""
2On the command line, use the following with your source files appended:
3
4cobra -ref:System.Windows.Forms -ref:System.Drawing ObjectExplorer-WinForms.cobra
5
6See Cobra's CobraMain-ObjectExplorer-WinForms.cobra for an example of using this Object Explorer.
7"""
8
9use System.Drawing
10use System.Windows.Forms
11use System.Reflection
12
13
14class ObjectExplorer inherits Form
15    implements ITreeBuilder, HasAppendKeyValue
16    """
17    Shows a tree view of one or more objects on the right and their properties. By drilling down
18    in the tree view, you can explore an object graph. On the right, the details of the currently
19    selected object are provided in a property grid format.
20
21    Because a PropertyGrid is used on the right, you can actually modify the objects. For example,
22    you can drill down to UI -> Form and increase the font which will immediately affect form and
23    all its controls while you're using them. Likewise, with your own objects.
24
25    For an example use, see CobraMain-ObjectExplorer-WinForms.cobra.
26
27    TODO
28        [ ] Search for text anywhere in the object graph
29            [ ] This technically works now, but is crazy slow. Not to mention the level > 10 hack!
30        [-] In the tree, show the elements of sets. Maybe that's an ICollection thing...
31            May already be done. Just added ICollection today 2008-12-06.
32        [ ] Add a context menu to the textbox with commands: &Copy, Select &All, &Toggle String Literals.
33            The last alters _willShowPlainTextInTextBox
34        [ ] Change the path to a text field that can be copied and pasted
35        [ ] Add an "Up" button to back up in the tree view? Maybe. Backspace in the treeview already does this.
36        [ ] May wish to replace the ListView with a DataGridView to get more comfortable spacing between text and grid lines
37
38    IDEAS
39        [ ] Bookmarks
40            - For jumping back and forth
41            - Could even persist (via the path) between sessions
42        [ ] Sort the properties 'logically', like all the primitives together (bools, then ints, etc.) and alpha within,
43            and then object reference properties like .type
44            and then 'subnodes' type properties like lists
45    """
46
47    var _initialEntries as List<of dynamic?>
48    var _willShowPlainTextInTextBox = true
49    var _maxSearchSeconds = 15.0f
50
51    # top:
52    var _buttonStrip as ToolStrip
53    var _findStrip as ToolStrip
54    var _findText as ToolStripTextBox
55    var _infoStrip as LabelStrip
56    var _pathStrip as LabelStrip
57
58    # left side:
59    var _treeView as TreeView
60
61    # right side:
62    var _objectIdStrip as LabelStrip
63    var _textBox as TextBox
64    var _objectViewsTabControl as TabControl
65    var _objectListView as ListView
66    var _propertyGrid as PropertyGrid
67
68    cue init(entries as vari dynamic?)
69        .init
70        _initialEntries = List<of dynamic?>(entries)
71
72    cue init
73        base.init
74        _initialEntries = List<of dynamic?>()
75        .text = 'Object Explorer'
76        .startPosition = FormStartPosition.Manual
77        _initSize
78        _makeControls
79
80    def addKeyValue(key as String, value as dynamic?)
81        _initialEntries.add(key)
82        _initialEntries.add(value)
83
84    def onLoad(e as EventArgs?) is override, protected
85        base.onLoad(e)
86        _populateNav
87        _treeView.focus
88
89    def onActivated(e as EventArgs?) is override, protected
90        base.onActivated(e)
91
92    def _initSize
93        area = Screen.primaryScreen.workingArea
94        fraction = 0.80
95        x = (area.width  * (1.0 - fraction) / 2 + area.x) to int
96        y = (area.height * (1.0 - fraction) / 2 + area.y) to int
97        w = (area.width  * fraction) to int
98        h = (area.height * fraction) to int
99        .location = Point(x, y)
100        .size = Size(w, h)
101
102    def _makeControls
103        splitContainer = SplitContainer(parent=this, dock=DockStyle.Fill, fixedPanel=FixedPanel.Panel1)
104
105        _treeView = TreeView(dock=DockStyle.Fill, hideSelection=false, parent=splitContainer.panel1, pathSeparator=' / ')
106        listen _treeView.afterSelect, ref .treeViewAfterSelect
107        listen _treeView.beforeExpand, ref .treeViewBeforeExpand
108
109        detailsPanel = Panel(dock=DockStyle.Fill, parent=splitContainer.panel2)
110       
111        split = SplitContainer(dock=DockStyle.Fill, parent=detailsPanel, orientation=Orientation.Horizontal, fixedPanel=FixedPanel.Panel1)
112
113        _textBox = TextBox()
114        _textBox.multiline = true
115        _textBox.scrollBars = ScrollBars.Vertical
116        _textBox.wordWrap = true
117        _textBox.font = Font('Courier New', .font.size# * 1.25f
118        _textBox.height *= 3
119        textBoxHeight = _textBox.height
120        _textBox.readOnly = true
121        _textBox.dock = DockStyle.Fill
122        _textBox.parent = split.panel1
123
124        _objectViewsTabControl = TabControl(dock=DockStyle.Fill, parent=split.panel2)
125
126        page = TabPage('Key / Value')
127        _objectListView = ListView()
128        _objectListView.view = View.Details
129        _objectListView.gridLines = true
130        _objectListView.fullRowSelect = true
131        _objectListView.columns.add('Key')
132        _objectListView.columns.add('View')
133        _objectListView.dock = DockStyle.Fill
134        _objectListView.parent = page
135       
136        _objectViewsTabControl.tabPages.add(page)
137       
138        page = TabPage('Property Grid')
139        _propertyGrid = PropertyGrid(dock=DockStyle.Fill, parent=page)
140       
141        _objectViewsTabControl.tabPages.add(page)
142       
143        split.splitterWidth += 2
144        split.splitterDistance = textBoxHeight
145
146        _objectIdStrip = LabelStrip(dock=DockStyle.Top, parent=detailsPanel)
147       
148        _infoStrip = LabelStrip(dock=DockStyle.Top, parent=this)
149
150        _pathStrip = LabelStrip('Path: ', dock=DockStyle.Top, parent=this)
151
152        _findStrip = ToolStrip(dock=DockStyle.Top, parent=this)
153        _findStrip.items.add(ToolStripLabel('&Find: '))
154
155        _findText = ToolStripTextBox()
156        _findText.width *= 2
157        listen _findText.keyPress, ref .findTextKeyPress
158        _findStrip.items.add(_findText)
159
160        butt = ToolStripButton(text='Find Next')
161        listen butt.click, ref .findNextClick
162        _findStrip.items.add(butt)
163       
164        #butt = ToolStripButton()
165        #butt.text = 'Find Previous'
166        #listen butt.click, ref .findPreviousClick
167        #_findStrip.items.add(butt)
168
169        _buttonStrip = ToolStrip(dock=DockStyle.Top, parent=this)
170
171        butt = ToolStripButton(text='&Copy')
172        listen butt.click, ref .copyClick
173        _buttonStrip.items.add(butt)
174       
175        butt = ToolStripButton(text='&Next')
176        listen butt.click, ref .nextClick
177        _buttonStrip.items.add(butt)
178       
179        butt = ToolStripButton(text='&Quit')
180        listen butt.click, ref .quitClick
181        _buttonStrip.items.add(butt)
182       
183        butt = ToolStripButton(text='&Reset')
184        listen butt.click, ref .resetClick
185        _buttonStrip.items.add(butt)
186
187    def copyClick(sender, e as EventArgs)
188        sb = StringBuilder()
189        sb.appendLine(_pathStrip.text)
190        if _treeView.selectedNode
191            sb.appendLine(_treeView.selectedNode.text)
192        s = sb.toString
193        if s.trim <> ''
194            Clipboard.setDataObject(s, true)
195
196    def nextClick(sender, e as EventArgs)
197        curNode = origNode = _treeView.selectedNode
198        if curNode.nodes and curNode.nodes.count
199            if not curNode.isExpanded
200                curNode.expand
201            next = curNode.nodes[0]
202        else
203            next = curNode.nextNode
204            while next is nil and curNode  # may have to go up more than one parent
205                curNode = curNode.parent
206                if curNode, next = curNode.nextNode
207            if next is nil and curNode is nil
208                next = _treeView.nodes[0]  # back to the top
209        if next and next is not origNode
210            _treeView.selectedNode = next
211            _treeView.select
212
213    def resetClick(sender, e as EventArgs)
214        _treeView.beginUpdate
215        try
216            _treeView.nodes.clear
217            _populateNav
218        finally
219            _treeView.endUpdate
220        if _treeView.nodes.count > 0
221            _treeView.selectedNode = _treeView.nodes[0]
222            _treeView.focus
223
224    def quitClick(sender, e as EventArgs)
225        Application.exit
226   
227    def findTextKeyPress(sender, e as KeyPressEventArgs)
228        if e.keyChar == 13 to char
229            .findNextClick(sender, EventArgs())
230
231    def findNextClick(sender, e as EventArgs)
232        # Although this avoids cycles in a single search operation, it does not avoid them in between
233        # search operations. We'll see if that's a problem in practice.
234        _infoStrip.text = 'Searching...'
235        _infoStrip.update
236        if _treeView.nodes.count == 0
237            return
238        start = DateTime.now
239        didFind = false
240        tv = _treeView
241        tv.beginUpdate
242        try
243            root = _treeView.nodes[0] to XTreeNode
244            selected = (_treeView.selectedNode ? root) to XTreeNode
245            text = _findText.text to !
246            for node in _nextNodes(root, selected, selected, Set<of dynamic>(), 0)
247                if node.contains(text)
248                    _treeView.selectedNode = node
249                    tv.endUpdate
250                    node.ensureVisible
251                    duration = DateTime.now.subtract(start)
252                    _infoStrip.text = 'Found in [duration.toString]'
253                    didFind = true
254                    break
255                if DateTime.now.subtract(start).totalSeconds > _maxSearchSeconds
256                    _infoStrip.text = 'Sorry, could not find text after [_maxSearchSeconds] seconds max search time.'
257                    return
258            if not didFind
259                _infoStrip.text = 'Could not find text.'
260        finally
261            tv.endUpdate
262
263    def _nextNodes(root as XTreeNode, original as XTreeNode, curNode as XTreeNode?, visited as Set<of dynamic>, level as int) as IEnumerable<of XTreeNode>
264        """
265        A
266        |
267        |--- B
268        |    |
269        |    |--- C
270        |    |
271        |    |--- D
272        |    |
273        |    `--- E
274        |
275        |--- F
276        |
277        |--- G
278        |
279        H
280        |
281        I
282        """
283        if level > 10, yield break  # OMGHACK
284        if curNode is nil, yield break
285        if curNode.value is not nil
286            if curNode.value in visited, yield break
287            visited.add(curNode.value to !)
288        if curNode.nodes and curNode.nodes.count
289            if (curNode.nodes[0] to XTreeNode).isDummy
290                _populateSubNodes(curNode to !)
291            for subNode as XTreeNode in curNode.nodes
292                if subNode.value is nil or not subNode.value in visited
293                    yield subNode
294                    for n in _nextNodes(root, original, subNode, visited, level+1)
295                        yield n
296        while true
297            next = curNode.nextNode to XTreeNode?
298            while next is nil and curNode  # may have to go up more than one parent
299                curNode = curNode.parent to XTreeNode?
300                if curNode, next = curNode.nextNode to XTreeNode?
301            if next is nil and curNode is nil
302                next = root  # back to the top
303            if next and next is not original
304                if next.value is nil or not next.value in visited
305                    yield next to !
306                    for n in _nextNodes(root, original, next, visited, level+1)
307                        yield n
308                    curNode = next
309            break
310
311    def findPreviousClick(sender, e as EventArgs)
312        MessageBox.show(this, 'Not implement yet.')
313       
314    def _populateNav
315        tv = _treeView
316        tv.beginUpdate
317        try
318            for i in _initialEntries.count
319                if i % 2 == 0
320                    key = _initialEntries[i]
321                    value = _initialEntries[i+1]
322                    node = XTreeNode(key to String, key to String, value)
323                    tv.nodes.add(node)
324                    node.nodes.add(XTreeNode.newDummyNode)
325            _populateNavUI 
326        finally
327            tv.endUpdate
328
329    def _populateNavUI
330        tv = _treeView
331
332        uiNode = XTreeNode('UI')
333        tv.nodes.add(uiNode)
334       
335        node = XTreeNode('This Form', '', this)
336        uiNode.nodes.add(node)
337        node.nodes.add(XTreeNode.newDummyNode)
338
339        node = XTreeNode('TreeView', '', _treeView)
340        uiNode.nodes.add(node)
341        node.nodes.add(XTreeNode.newDummyNode)
342       
343        node = XTreeNode('Key Value View', '', _objectListView)
344        uiNode.nodes.add(node)
345        node.nodes.add(XTreeNode.newDummyNode)
346       
347        node = XTreeNode('PropertyGrid', '', _propertyGrid)
348        uiNode.nodes.add(node)
349        node.nodes.add(XTreeNode.newDummyNode)
350       
351        node = XTreeNode('PrimaryScreen', '', Screen.primaryScreen)
352        uiNode.nodes.add(node)
353        node.nodes.add(XTreeNode.newDummyNode)
354
355    def treeViewAfterSelect(sender, e as TreeViewEventArgs)
356        # update the details view
357        obj = (e.node to XTreeNode).value
358        _objectIdStrip.text = if(obj, .objectIdTextFor(obj), '')
359        _updatePathStrip
360        if _willShowPlainTextInTextBox
361            _textBox.text = if(obj inherits String, obj, CobraCore.toTechString(obj))
362        else           
363            _textBox.text = CobraCore.toTechString(obj)
364        # object views on right hand side
365        _populateObjectListView(obj)
366        _propertyGrid.selectedObject = obj
367
368    def _populateObjectListView(obj as Object?)
369        listView = _objectListView
370        listView.beginUpdate
371        try
372            listView.items.clear
373            for info in _keyValuesOf(obj)
374                key = info[0] to String
375                value = info[1]
376                # isGood = info[2] to bool
377                item = ListViewItem(key)
378                item.subItems.add(CobraCore.toTechString(value))
379                listView.items.add(item)
380            for i in 2, _objectListView.autoResizeColumn(i, ColumnHeaderAutoResizeStyle.ColumnContent)
381        finally
382            listView.endUpdate
383
384    def _updatePathStrip
385        node = _treeView.selectedNode to XTreeNode?
386        nodes = List<of XTreeNode>()
387        while node
388            nodes.add(node)
389            node = node.parent to XTreeNode?
390        nodes.reverse
391        sb = StringBuilder()
392        for node in nodes
393            if sb.length and not node.propertyName.startsWith('.') and not node.propertyName.startsWith(r'[')
394                sb.append('.')
395            sb.append(node.propertyName)
396        _pathStrip.text = sb.toString
397
398    def objectIdTextFor(obj as dynamic) as String
399        """
400        Subclasses can override this method to customize the text that appears in the 'object id' strip in the details view.
401        The default implementation gives the type name and--if they exist--the .serialNum and the .name or .fileName of the object.
402        """
403        s = CobraCore.typeName(obj.getType)
404        if .isPrimitive(obj)
405            s += ' ' + CobraCore.toTechString(obj)
406        else
407            propInfo = obj.getType.getProperty('SerialNum')
408            if propInfo
409                try
410                    sn = propInfo.getValue(obj to Object, nil) ? ''
411                    s += '.' + sn.toString
412                catch
413                    pass
414            propInfo = obj.getType.getProperty('Name')
415            if propInfo
416                try
417                    name = propInfo.getValue(obj, nil)
418                    s += ' ' + CobraCore.toTechString(name)
419                catch
420                    pass
421            else
422                propInfo = obj.getType.getProperty('FileName')
423                if propInfo
424                    try
425                        fileName = propInfo.getValue(obj, nil)
426                        s += ' ' + CobraCore.toTechString(fileName)
427                    catch
428                        pass
429        return s
430
431    def treeViewBeforeExpand(sender as Object, e as TreeViewCancelEventArgs)
432        node = e.node to XTreeNode
433        if node.value and node.nodes.count > 0 and (node.nodes[0] to XTreeNode).isDummy
434            _populateSubNodes(node)
435
436    def _populateSubNodes(node as XTreeNode)
437        require
438            node.value
439            node.nodes.count > 0
440            (node.nodes[0] to XTreeNode).isDummy
441        ensure
442            node.nodes.count == 0 or not (node.nodes[0] to XTreeNode).isDummy
443        body
444            node.nodes.removeAt(0)  # the dummy node
445            obj = node.value
446            for info in _keyValuesOf(obj)
447                key = info[0] to String
448                value = info[1]
449                isGood = info[2] to bool
450                child = XTreeNode('[key] == [CobraCore.toTechString(value)]', key, value)               
451                if isGood
452                    if not .isPrimitive(value)
453                        child.nodes.add(XTreeNode.newDummyNode)
454                node.nodes.add(child)
455            if obj inherits AssertException  # TODO: Generalize to some interface like HasPopulateTreeWithNonPropertyNodes
456                _nodeStack = Stack<of XTreeNode>()
457                _nodeStack.push(node)
458                _appendMode = 1
459                try
460                    obj.populateTreeWithExpressions(this)
461                finally
462                    _appendMode = 0
463                _nodeStack = nil
464           
465    def _keyValuesOf(obj as dynamic?) as IEnumerable<of List<of dynamic?>>
466        """
467        Yields a series of [keyName, value, isGood] for the given object.
468        The series includes properties, indexed elements of IList and keyed elements of IDictionary.
469        The keyName is a string. The value could be anything including nil.
470        When isGood is false, an exception was caught when retrieving the value and consequently the value says 'Caught during...'.
471       
472        This method can be used to populate a detailed view of the object, a list of subnodes in a treeview, etc.
473       
474        Does *not* check for AssertException to invoke any of its special methods for displaying subexpressions.
475        """
476        if obj is nil, yield break
477        yield ['.getType', obj.getType, true]
478        yield ['.toTechString', obj, true]
479        propInfos = List<of PropertyInfo>((obj to Object).getType.getProperties)
480        propInfos.sort(ref .comparePropInfo)
481        for propInfo in propInfos
482            if propInfo.name == 'Item'  # used for indexing. technically could be named something else, but this works in practice
483                continue
484            value, isGood = nil, false
485            try
486                value = propInfo.getValue(obj, nil)
487                isGood = true
488            catch exc as Exception
489                if exc inherits TargetInvocationException and exc.innerException
490                    exc = exc.innerException to !
491                value = 'Caught during get: [exc.getType.name]: [exc.message]'
492            propName = .cobraMemberNameFor(propInfo.name)
493            yield [propName, value, isGood]
494        for kv in _keyValueContentOf(obj)
495            yield kv
496
497    var _keyValueContents = List<of KeyValuePair<of String, dynamic?>>()
498   
499    def _keyValueContentOf(obj as dynamic?) as IEnumerable<of List<of dynamic?>>
500        lb = c'['
501        if obj inherits System.Collections.IList
502            for i in obj.count
503                propName = '[lb][i]]'
504                value, isGood = nil, false
505                try
506                    # CC: value, isGood = obj[i], true
507                    value = obj[i]
508                    isGood = true
509                catch exc as Exception
510                    value = 'Caught during IList[propName]: [exc.getType.name]: [exc.message]'
511                yield [propName, value, isGood]
512        else if obj inherits System.Collections.IDictionary
513            keys = System.Collections.ArrayList(obj.keys)
514            try, keys.sort
515            catch InvalidOperationException, pass
516            for dictKey in keys
517                propName = '[lb][CobraCore.toTechString(dictKey)]]'
518                value, isGood = nil, false
519                try
520                    # CC: value, isGood = obj[dictKey], true
521                    value = obj[dictKey]
522                    isGood = true
523                catch exc as Exception
524                    value = 'Caught during IDictionary[propName]: [exc.getType.name]: [exc.message]'
525                lb = c'['
526                yield [propName, value, isGood]
527        else if obj inherits System.Collections.ICollection
528            items = for item in obj get item  # make a list
529            if obj.getType.isGenericType and obj.getType.getGenericTypeDefinition.name == 'Stack`1'  # TODO: name comparison feels hacky
530                items.reverse
531            for entry in _keyValueContentOf(items)  # recursive call on list
532                yield entry
533        if obj inherits HasAppendNonPropertyKeyValues
534            _keyValueContents.clear
535            _appendMode = 3
536            try
537                obj.appendNonPropertyKeyValues(this)
538                for kv in _keyValueContents
539                    yield [kv.key, kv.value, true]
540            finally
541                _appendMode = 0
542                _keyValueContents.clear
543       
544    def comparePropInfo(a as PropertyInfo, b as PropertyInfo) as int
545        return a.name.toLower.compareTo(b.name.toLower)
546
547    def cobraMemberNameFor(name as String) as String
548        return if(name[0]=='_', '', '.') + name[0].toLower.toString + name[1:]
549
550    def isPrimitive(value as dynamic?) as bool
551        if value is nil, return true
552        if value inherits bool, return true
553        if value inherits char, return true
554        if value inherits decimal, return true
555        if value inherits int, return true
556        if value inherits float, return true
557        if value inherits String, return true
558        return false
559
560    ## ITreeBuilder
561   
562    # These methods are invoked by AssertException to populate the tree nodes for subexpressions of the assert condition.
563   
564    var _nodeStack as Stack<of XTreeNode>?
565        """
566        A stack of 'parent nodes' produced during ITreeBuilder calls such as .appendKeyValue and .indent.
567        """
568
569    var _appendMode = 0
570
571    def indent
572        nodes = _nodeStack.peek.nodes
573        assert nodes.count > 0, 'Cannot indent more than once.'
574        # make the last sibling node the new parent
575        _nodeStack.push(nodes[nodes.count-1] to XTreeNode)
576
577    def outdent
578        _nodeStack.pop
579
580    def appendKeyValue(key as String, value)
581        branch _appendMode
582            on 1
583                text = '[key] == [CobraCore.toTechString(value)]'
584                node = XTreeNode(text, key, value)
585                _nodeStack.peek.nodes.add(node)
586            on 2
587                item = ListViewItem(key)
588                item.subItems.add(CobraCore.toTechString(value))
589                _objectListView.items.add(item)
590            on 3
591                _keyValueContents.add(KeyValuePair<of String, dynamic?>(key, value))
592            else
593                throw FallThroughException(_appendMode)
594
595
596class XTreeNode
597    inherits TreeNode
598    """
599    The major properties of interest are:
600        .text - the display text seen on screen. usually '.propertyName = value'
601        .propertyName - the name of the property that this node represents for its parent
602        .value - the value of the node. usually the value of a property
603
604    Top level nodes are not based on properties and will have empty strings for their .propertyName.
605    """
606
607    shared
608
609        def newDummyNode as XTreeNode
610            return XTreeNode('dummy', '', .dummyTag)
611
612        get dummyTag is protected
613            return '-- dummy tag --'
614           
615    var _propertyName as String
616    var _value as dynamic?
617   
618    cue init(text as String?)
619        .init(text, '', nil)
620
621    cue init(text as String, propertyName as String, value as dynamic?)
622        base.init(text)
623        _propertyName = propertyName
624        _value = value
625
626    get isDummy as bool
627        return .value is .dummyTag
628
629    pro propertyName from var
630   
631    pro value from var
632
633    def contains(s as String) as bool
634        return .text.toLower.contains(s.toLower)
635
636
637class LabelStrip
638    inherits ToolStrip
639    """
640    You can set the contents of a label strip directly:
641        labelStrip.text = 'some message'
642
643    The label strip also maintains a prefix string which is blank by default.
644    """
645
646    var _label as ToolStripLabel
647    var _prefix as String
648   
649    cue init
650        .init('')
651
652    cue init(prefix as String)
653        base.init
654        _label = ToolStripLabel(prefix)
655        .items.add(_label)
656        _prefix = prefix
657
658    pro prefix from var
659
660    pro text as String? is override
661        get
662            return _label.text to !
663        set
664            _label.text = _prefix + (value ? '')
Note: See TracBrowser for help on using the browser.