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

Revision 2151, 20.2 KB (checked in by Chuck.Esterbrook, 8 months 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.