Marple is a Swing-based Java library for analyzing Swing applications. Notable features are:
- Analysis of hierarchical structure of Swing components including field search
- Component, image, and color previews when analyzing fields
- Highlighting of components referenced by fields
- Analysis support for
Iterable
s, arrays, andMap
s - Expression evaluation in the context of selected components
- Instance-based search
- Support for data exchange with debuggers
- Target
- Features
- Default Shortcuts
- Setup and Configuration
- Navigable Elements
- Actions
- Inspection Dialog
- Evaluation Dialog
- Snapshot Dialog
- Debug Support
- Custom Actions
- Search Dialog
- Open Source License Acknowledgement
Marple has been developed for two reasons:
-
To analyze the UI structure of Swing applications. One aspect is the analysis of the component hierarchy described by the parent-child relationship between Swing components. The second aspect is analyzing how the hierarchy is represented by the code. For this, Marple scans fields values of components in the hierarchy and matches them against other components. By doing so, anonymous object references in the JVM are mapped to named fields in the source code. This can help developers to understand how the UI code matches the resulting visualization.
-
For some applications, the UI is a good entry point for debugging. This is particularly the case if there is a connection between the Swing components and core classes via object references. Marple provides code evaluation facilities via the Zenodot library, which allows developers to evaluate code in the context of a selected Swing component.
To start the hierarchical structure analysis, hover over the component you want to analyze and then press the inspection shortcut. The default shortcut is Ctrl + Shift + I
. The inspection dialog pops up. The dialog contains multiple tabs. The first tab "Component Hierarchy" shows a linear tree from the Swing root component to the component under the mouse. Marple also searches for fields that reference the components in this tree. This is particularly interesting because you get information about where in the code which part of this hierarchy is generated.
Marple can display previews of JComponent
s, Image
s, and Paint
s, which includes Color
s. The snapshot dialog is opened via the snapshot action. One handy feature are live previews that are created when hovering over navigable elements.
While the component hierarchy tab helps you finding GUI elements in your code, the highlight component action helps you finding components you have encountered during your analysis on the screen by letting it flash multiple times.
The "Iterables" tab in the inspection dialog offers common operations for Iterable
s and arrays: Filter, Map, ForEach, Collect, ToMap, Count, and Group. Similarly, the "Maps" tab offers common operations for Map
keys and values: Filter and Map.
Marple allows you to evaluate expressions. These expressions are evaluated in a certain context. This context can be referred to by the keyword this
. Instead of this.myField
or this.myMethod()
you can simply write myField
or myMethod()
when referring to the field myField
or the method myMethod()
of the context object. Some notable features for expression evaluation are:
- Different evaluation modes: You can choose between static typing, dynamic typing, and a mixed mode. With dynamic typing, runtime types rather than declared types are used when evaluating expressions and suggesting code completions. This can save tedious casts.
- Class and package imports
- Definition of variables that can be used within expressions
- A language extension that allows navigating in a custom, application-specific hierarchy
Expressions are evaluated in the evaluation dialog.
Marple allows you to find paths from one instance to another instance or all instances of a certain class satisfying a specific search criterion. If the field of an instance x
references and instance y
, then (x, y)
is considered a (directed) edge in the search graph from node x
to node y
. The search can be used to analyze memory leaks or to track where certain parameters given to a method are internally stored.
See Search Dialog for details.
The in-application debugging capabilities are a nice feature, but they are negligible compared to what real debuggers are capable of. Having that said, Marple is still valuable for your debugging process because it gives you good entry points via the GUI components of your application. Data exchange between Marple and the debugger is done via the debug support dialog.
The GUI elements of your application are the entry points for the analysis with Marple. From there you can navigate along fields in the inspection dialog, analyze the result of expressions in the evaluation dialog, or search for instances in the search dialog. Many Marple elements provide a context menu that offers several options to continue the investigation. These elements are called "navigable elements".
Additionally, the inspection and the evaluation dialog maintain a history of inspected or evaluated objects, respectively. This allows you to continue your investigation at an object you have analyzed earlier or to look something up in the history and then continue your current investigation.
All shortcuts can be configured. Unless changed, the shortcuts are as follows:
Shortcut | Action |
---|---|
Ctrl + F1 |
Quick Help dialog showing the actual shortcuts |
Ctrl + Shift + I |
Inspection dialog |
Alt + F8 |
Expression Evaluation dialog |
Ctrl + Shift + F |
Find Instance dialog |
Ctrl + Shift + D |
Debug Support dialog |
Ctrl + Shift + A |
Custom Actions dialog |
Ctrl + Space |
Suggest code completions for expressions |
Ctrl + P |
Show method argument types |
If you cannot remember the shortcuts, then you can look them up in the Quick Help that displays the actual shortcuts:
Hence, the shortcut for the Quick Help is the only one you have to know initially.
To set up Marple, you have to
- create and configure an
InspectionSettingsBuilder
, - build
InspectionSettings
by callingInspectionSettingsBuilder.build()
, and - register the settings via
ObjectInspectionFramework.register(inspectionSettings)
.
An InspectionSettingsBuilder
can be created by either calling
InspectionSettingsBuilder.create()
orObjectInspectionFramework.createInspectionSettingsBuilder()
.
The first call creates an empty builder, while the second call creates a preconfigured builder that
- recognizes nodes of
JTree
s, items ofJList
s, and cells ofJTable
s as subcomponents, - defines text representations of several Swing components, and
- registers the fields tab, the methods tab, the iterables tab, and the maps tab for the inspection dialog.
InspectionSettings
consist of
- an immutable part that has to be configured by the
InspectionSettingsBuilder
and - a mutable part that is intended to be configurable by the user.
The only mutable settings are the settings for the parser that can be configured in the evaluation settings dialog and the settings for custom actions that can be defined in the custom actions dialog. If a preferences file has been specified via InspectionSettingsBuilder.preferencesFile()
, then these settings will also be stored and loaded when the next session is started. Note that Marple will use a file with a similar name for storing its preferences and not exactly the specified one because different Marple versions that use different preferences file formats must write into different preferences files.
In the remainder of this section we discuss the different types of settings.
Marple lets you analyze, among others, the hierarchy of Swing components. However, not every graphic element is a full-blown Swing component. We call such elements "subcomponents". It makes sense to see subcomponents in the hierarchy as well. By default, Marple supports the following subcomponents:
- nodes in a
JTree
- items in a
JList
- cells in a
JTable
Subcomponents might also be structured hierarchically. These structures are described by the interface ComponentHierarchyModel
. Instances of ComponentHierarchyModel
are created via the ComponentHierarchyModelBuilder
. There are different ways to create a builder instance:
- The method
ComponentHierarchyModelBuilder.create()
creates an empty builder. - The method
ObjectInspectionFramework.createComponentHierarchyModelBuilder()
that already supports the aforementioned subcomponents.
The builder allows you to add SubcomponentHierarchyStrategy
instances that return a list of subcomponents for a given component and a given point inside the component. Since in many cases there will be at most one subcomponent and no hierarchy of subcomponents, there is a utility method ComponentHierarchyModels.createSingleSubcomponentStrategy
that simplifies the creation of such strategies a bit. For the registration of nodes in a JTree
, the code could look as follows:
builder.subcomponentHierarchyStrategy(JTree.class, createSingleSubcomponentStrategy((tree, point) -> {
TreePath path = tree.getPathForLocation(point.x, point.y);
return path == null ? null : path.getLastPathComponent();
}));
Note that the bi-function that you have to specify for the method createSingleSubcomponentStrategy()
may return null
if there is no subcomponent for the specified position.
The interface EvaluationSettings
contains information about what will be displayed when the evaluation dialog is opened for certain objects. Additionally, it tells Marple how to let the user configure additional settings when evaluating expressions that are beyond the core of Marple and Zenodot.
You can create an instance of EvaluationSettings
via its builder that you obtain via EvaluationSettingsBuilder.create()
.
The EvaluationSettingsBuilder
provides 3 methods:
-
The method
suggestExpressionToEvaluate
lets you specify which expression to display initially for which object. When nothing is specified for a certain object, thenthis
is displayed. -
The method
addRelatedObjectsProvider
lets you specify which related objects to show in a dedicated panel when evaluating a certain object. -
The method
setAdditionalSettings
lets you specify how users can specify certain additional settings via UI that influence, e.g., how expressions are evaluated or how code completions are generated. One example that calls this method is the Marple extension "Directory Completions UI". The UI component for the user is provided byAdditionalEvaluationSettings.createSettingsComponent()
and will be displayed as tab in the evaluation settings dialog (see Evaluation Settings).
The interface VisualSettings
describes how to display objects as text and which tabs are available in the inspection dialog for which object. You can create an instance of VisualSettings
via the VisualSettingsBuilder
. There are different ways to create a VisualSettingsBuilder
:
- The method
VisualSettingsBuilder.create()
creates an empty builder. - The method
ObjectInspectionFramework.createVisualSettingsBuilder()
creates a builder with the default settings used by Marple.
You can also create an empty builder and call VisualSettingsUtils.addDefaultDisplayTextFunctions(builder)
or VisualSettingsUtils.addDefaultViews(builder)
to add the Marple default settings of only one of the two aspects of VisualSettings
.
The VisualSettingsBuilder
provides the following methods:
-
The method
nullDisplayText
lets you specify a String that is used to display the valuenull
. -
The method
displayText
lets you specify how to display objects of a certain class. Marple currently defines special logic to display instances of the following classes:String
,char
/Character
,Object
,Frame
,AbstractButton
,JLabel
, andJTextComponent
. A sample usage of the method isbuilder.displayText(char.class, c -> "'" + c + "'")
. -
The method
objectView
lets you specify constructors/factory methods forObjectView
instances for a certain type of object. EachObjectView
describes one tab in the inspection dialog. The available tabs depend on the inspected object. Note that the class argument only serves as a first filter for deciding which tabs to create and which not. The factory method you provide may returnnull
if there should be no tab for a given object although the class is as expected. -
The methods
listView
,iterableView
, andmapView
let you specify alternative views (asList
, asIterable
, or asMap
) on objects of certain classes. Additionally, you have to specify whether this should be the default view or not. These alternative views serve two purposes:- When viewing an object, you can switch between the regular object view and the alternative view(s).
- When you provide one of these views for a certain object class, then the "Iterables" tab or the "Maps" tab will be available in the Inspection Dialog even if they don't implement one of these interfaces inherently.
Instances of ObjectView
describe tabs in the inspection dialog. The method getViewName()
is used to determine the tab title. The method getViewComponent()
returns the component that is displayed on the tab.
ObjectView
s provide a way to store and load their current settings via the methods getViewSettings()
and applyViewSettings()
. There are two scenarios when view settings are applied:
-
When navigating in the inspection history by using the "Back" and "Forward" buttons, then the view settings associated with the element in the history are restored.
-
When inspecting a new object, then the current view settings are applied as good as possible and reasonable to the view for the new object.
As a rule of thumb, general settings are always applied, whereas object-dependent settings are only applied in the first scenario. To distinguish between both scenarios, the method applyViewSettings()
gets an argument of type ObjectView.ViewSettingsOrigin
describing where the view settings stem from: ObjectView.ViewSettingsOrigin.SAME_CONTEXT
refers to the first scenario, in which all settings should be applied. ObjectView.ViewSettingsOrigin.OTHER_CONTEXT
refers to the second scenario, in which only general settings should be transferred.
When the dialog that holds the ObjectView
's view component is closed, then the method ObjectView.dispose()
is called which is meant for cleanup.
You can extend Marple by defining custom actions. Custom actions are essentially expressions that are evaluated in the context of a certain object that can be referred to via the literal this
. For each action you can specify a name, for which types of objects it is applicable, and optionally a keyboard shortcut. When using the keyboard shortcut, the GUI element under the mouse cursor will be used as the object for which the expression is evaluated.
Custom actions also appear in the context menu (cf. navigable elements). When executing them via such a menu item, then the object for which the context menu has been created will be the one the expression is evaluated for.
Note that the user can change custom actions in the custom action dialog.
Earlier versions of Marple contained a feature called "breakpoint trigger". There you could specify a runnable that has been executed when pressing a certain button in the "Debug Settings" dialog. The idea is that at some point you want to trigger a breakpoint just to start debugging the current state in your IDE. So you just had to set a breakpoint in a static method of choice and call this via the breakpoint trigger feature.
Although we still find this feature valuable, we decided that it could be more general and flexible. This is why we dropped that feature and added custom actions instead. With custom actions you can mimic breakpoint triggers:
The class DebugSupport
has a method triggerBreakpoint(Object)
that we want to use for our example. You need to do the following in order to define a custom action that calls this method:
CustomAction triggerBreakpointAction = CustomAction.create(
"Trigger breakpoint",
"dd.kms.marple.api.DebugSupport.triggerBreakpoint(this)",
Object.class,
new KeyRepresentation(KeyEvent.CTRL_MASK | KeyEvent.SHIFT_MASK, KeyEvent.VK_B)
);
CustomActionSettings customActionSettings = CustomActionSettings.of(triggerBreakpointAction);
InspectionSettings inspectionSettings = ObjectInspectionFramework.createInspectionSettingsBuilder()
.customActionSettings(customActionSettings)
.build();
This defines a custom action "Trigger breakpoint" that is defined on arbitrary Object
s and can also be triggered by pressing Ctrl + Shift + B. Note that the expression is quite cumbersome because the class DebugSupport
is not imported by default. You could change this by adapting the parser settings appropriately.
This custom action will then also appear in the context menu:
If you decide to ship Marple with your application, then you might want to prevent your customers from analyzing your application with Marple. To do so, you can subclass the interface SecuritySettings
to protect the Marple access by a password prompt. Marple does neither restrict the way the password prompt is implemented nor does it provide utilities for implementing it.
The interface KeySettings
contains all information about configurable shortcuts. You can create a KeySettings
instance with a KeySettingsBuilder
. The method KeySettingsBuilder.create()
creates such a builder, preconfigured with the default shortcuts.
Parser/evalution settings are partially configurable by the user. These settings control how expressions are evaluated. In most cases it is helpful to preconfigure these settings appropriately. You can set the parser settings of an instance of InspectionSettings
as follows:
inspectionSettings.getEvaluator().setParserSettings(parserSettings)
ParserSettings
are generated by the ParserSettingsBuilder
which is created by ParserSettingsBuilder.create()
.
The builder allows you, among others,
- to configure the behavior of code completions (currently not configurable via API)
- to specify the imports (classes and packages),
- to specify the minimum access modifier (
private
,package private
,protected
, orpublic
) a field, method, or constructor must have in order to be accessed from within expressions - to choose the evaluation mode
See the parser settings documentation of Zenodot for more details on these features.
Marple uses GUI components of your application as entry points for you analysis. However, you are not bound to these components. Most of the dialogs contain navigable elements. These elements provide a context menu if you right-click onto them. The "inspect" action is the default action which will be executed when you left-click onto a navigable element. In this section we show examples of navigable elements:
-
Nodes in the "Component Hierarchy" tab in the inspection dialog:
-
Nodes in the quick view and values in the detailed view of the "Fields" tab in the inspection dialog:
-
Nodes in the quick view and methods in the detailed view of the "Methods" tab in the inspection dialog:
-
Nodes in all result panels like in the evaluation dialog:
-
Values in the variables tab in the settings for expression evaluation:
-
Values of unnamed and named slots in the debug support dialog:
Navigable elements provide several actions that can be selected from the context menu that pops up when right-clicking onto such an element. See Navigable Elements for example context menus.
The "Inspect" action opens the inspection dialog with the object represented by the navigable element as context.
The "Highlight Component" action is only available for Component
s. When executing this action, then the component flashes repeatedly for a short time span. This is helpful if you are exploring fields of a class and want to know where the component this field points to is located on the screen. Of course this only works if the component is currently visible on the screen.
The "Snapshot" action is available for JComponent
s, Image
s, Icon
s, and Paint
s. This action opens the snapshot dialog that displays the component/image/icon/paint and allows you to save it or copy it to the clipboard.
You can introduce variables to reference instances within expressions. This is done by the "Add to variables" action.
The "Evaluate" and "Evaluate XXX" action both open the evaluation dialog. The only difference is that the "Evaluate" action opens the dialog using the object that is represented by the navigable element as context that can be referred to by the keyword this
. In contrast to this, the "Evaluate XXX" action, which is not always available, used a context in which XXX refers to the object.
Example: Consider the detailed view in the "Fields" tab of the inspection dialog:
Assume that a JButton
is the current context of the inspection dialog and consider the navigable element for the field actionMap
. The "Evaluate" action will open the evaluation dialog with the value of the field actionMap
as context, i.e., the keyword this
will refer to actionMap
. The "Evaluate 'actionMap'" action will open the evaluation dialog with the JButton
as context. In that case, the keyword this
refers to the button and the expression actionMap
, which will be the initial text in the expression text field, will refer to actionMap
.
Both actions open or configure the open search dialog where you can search instances from a start instance. Depending on which action you have chosen, the object of the navigable element will be used as start or as target instance for the search dialog.
This action copies the value returned by toString()
of the object represented by the navigable element to the clipboard.
This action opens the debug support dialog.
This action is only available if the object also has a different view (a List
view, an Iterable
view, or a Map
view), which is currently shown. Objects with such a view are arrays, List
s, Iterable
s, Map
s, but also objects for which such an alternative view has been defined (see Visual Settings).
This action is only available if the object has a List
view, an Iterable
view, or a Map
view (cf. View as Object Action) which is currently not shown.
This action is only available for navigable elements that represent methods without argument. When this action is executed, then this method is executed and its result is used as new context of the inspection dialog.
The inspection dialog is a tool to analyze objects. There are two ways to open this dialog:
-
You can hover over a Swing component and press the inspection shortcut. The default shortcut is
Ctrl + Shift + I
. The inspection dialog becomes visible with the component as context, i.e., all tabs refer to this component. -
You can left-click onto any navigable element. The inspection dialog becomes visible with the object represented by that element as context.
The inspection dialog can consist of up to five predefined tabs:
- the "Component Hierarchy" tab,
- the "Fields" tab,
- the "Methods" tab,
- the "Iterables" tab, and
- the "Maps" tab.
The "Fields" and the "Methods" tab are always available. The "Component Hierarchy" tab is only available if the selected object is a Swing component or a supported subcomponent (see Component Hierarchy Model). The "Iterables" tab is only available if the selected object is an Iterable
or an array. The "Maps" tab is only visible if the selected object is a Map
.
You can explore related instances by clicking on
- component hierarchy tree nodes,
- fields, or
- methods without parameters.
When you do so, the tabs are updated showing the information for the newly selected instance. You can use the "Back" and "Forward" buttons to navigate in the history of selected instances. The history will be cleared when the inspection dialog is closed.
The "Component Hierarchy" tab is only visible if the selected object is a Swing component or a supported subcomponent (e.g. TreeNode
s). It is possible to extend Marple to recognize custom subcomponents (see Component Hierarchy Model). The tab shows a linear tree from the Swing root component to the selected (sub)component:
The node names in the tree contain the class names of the components. In some cases there will be additional information embraced in parentheses. These are fields of classes that reference the component the node represents. That way, you do not only obtain information about the hierarchical Swing component structure, but also about the code that generates this structure. Whenever you need to modify one of these components, you get a hint where to look in the code.
In the screenshot above one can see that the user tried to analyze a DefaultMutableTreeNode
. Marple does not only show that this is a node in an arbitrary JTree
, but that this JTree
instance is referenced by the field tree
of the class TestFrame
.
The "Fields" tab lists all fields of the selected object. Two views are available: In the "Quick View" you get an overview over all fields of the selected object in form of a tree. You can simply expand field nodes to analyze their fields:
If there are many fields and you have to search for a field, then the "Detailed View" may be the right option:
In the "Detailed View" you can sort fields by name, value, type, declaring class, or modifier. For this, you simply left-click onto the corresponding column header. You can also filter fields by specifying filters for one ore more columns. For this, right-click onto the corresponding column headers and configure the filter. There are three types of filters:
-
Text-based filters match String representations of objects against the specified filter text:
Text-based filters support camel case search ("aC" matches "accessibleContext" and "appContext") and wildcards ("*"). -
Selection-based filters provide a set of possible values from which you can select one or more values:
-
The modifier filter allows you to specify the minimum visibility a field should have in order to be shown in the table. Additionally, you can specify whether to show static fields or not:
Similar to the "Fields" tab, the "Methods" tab lists all methods of the selected object. In the "Quick View", you get an overview over all available methods. You can use the method name filter and the modifier filter to search for methods:
If you need more options for filtering, then you can use the "Detailed View":
Like in the "Detailed View" of the "Fields" tab, you can sort methods according to the criteria specified by the column headers by left-clicking onto these headers. You can also filter the methods by right-clicking onto the column headers and specifying a text-based, a selection-based, or a modifier filter, depending on the column you selected.
The "Iterables" tab is only visible if the selected object is an Iterable
or an array or if you have provided a List
or an Iterable
view for the class of this object (see Visual Settings). This tab provides common operations on Iterable
s (Filter, Map, ForEach, Collect, ToMap, Count, and Group). We will discuss these operations in the remainder and how to use them for a stream-like analysis.
You can specify an arbitrary predicate that is applicable to the elements of the Iterable
. When pressing the "Run" button, the Iterable
is filtered with this predicate and the result is displayed at the bottom.
The output (if any) can either be a list or an index map. If you are only interested in the filtered elements, then you should choose a list:
If you are interested in the original indices of the filtered elements, then you should select the index map:
As can be seen in the screenshot, the result is a map from original indices to values.
We decided to keep the number of operations at a minimum. Many other common operations can be solved via a filter:
contains
,anyMatch
,noneMatch
: Filter with the predicate and check whether the output is empty or not.allMatch
: Filter with the negated predicate and check whether the output is empty or not.find
/indexOf
: Select as output type an index map and filter with the predicate.
You can specify an arbitrary (unary) function that is applicable to the elements of the Iterable
. All elements will be mapped by this function and collected into a list:
In this example, each value is mapped to its square.
You can specify an arbitrary consumer that is applicable to the elements of the Iterable
. The consumer will subsequently be called for all elements:
In this example, all elements of the Iterable
have been printed to the console.
You can transform Iterables
and arrays to other Collection
s or arrays. For this, you have to specify an expression that constructs the desired Collection
/array:
It is not necessary to specify the correct size. You may, e.g., create an empty array. Marple will detect and set the size correctly.
You can transform Iterables
and arrays to a Map
by specifying a key mapping and a value mapping:
You can specify a mapping that assigns a group to each element of the Iterable
and then count the number of elements in each group:
In this example, the number of elements of the Iterable
that fall into the intervals [0.0, 0.1), [0.1, 0.2), ..., [0.9, 1.0), respectively, are counted. These intervals are represented by the numbers 0, 1, ..., 9.
The group operation is very similar to the count operation. The only difference is that here we are not interested in the number of elements in each group, but in the elements in these groups themselves:
In this example, all elements of the Iterable
that fall into the intervals [0.0, 0.1), [0.1, 0.2), ..., [0.9, 1.0), respectively, are lister per interval. These intervals are represented by the numbers 0, 1, ..., 9.
The "Iterables" tab provides a way to analyze Iterable
s similar to the streaming approach. The result of an operation is displayed at the bottom of the dialog. If it is a list (you will not use an index map as result type during streaming) and you click onto that list, then the inspection dialog uses that newly selected list as context and updates all tabs. Since that list is an Iterable
as well, you remain in the "Iterables" tab. Hence, you can apply the next streaming operation.
Note that with that workflow you benefit from the inspection history, which you can navigate in using the "Back" and "Forward" buttons.
Example: Consider the following stream operations on a double
array "values":
Arrays.stream(values)
.filter(x -> x > 0.999)
.map(Math::acos)
.forEach(System.out::println);
To resemble these in the "Iterables" tab, the following steps are required, assuming that "values" is the initially selected object:
-
Left-click onto the resulting
ArrayList
to use it as new context:
-
Left-click onto the resulting
ArrayList
to use it as new context:
The "Maps" tab is only visible if the selected object is a Map
or if you have provided a Map
view for the class of this object (see Visual Settings). This tab provides common operations on Map
s (Filter and Map):
You can specify arbitrary predicates that are applicable to the keys or values of the Map
, respectively. When pressing the "Run" button, the Map
is filtered with these predicates and the result is displayed at the bottom:
In the previous example, a new map will be created with those entries whose keys exceed 0.6 and with values whose third character is not 9. Note that always both, keys and values, are filtered. If you only want to filter keys (values), then you can specify the filter expression x -> true
for the values (keys).
You can specify arbitrary (unary) functions that are applicable to the keys and values of the Map
. All keys and values will be mapped by these functions and collected into a map:
In the example above, a new map will be created with the squares of the original keys as new keys and the lengths of the original values as new values. Note that always both, keys and values, are mapped. If you only want to map keys (values), then you can specify the mapping expression x -> x
for the values (keys).
The evaluation dialog allows you to evaluate expressions. There are two ways to open the evaluation dialog:
-
You can hover over a Swing component and press the evaluation shortcut. The default shortcut is
Alt + F8
. The evaluation dialog becomes visible with the component as context, i.e., the keywordthis
refers to that component. -
You can right-click on any navigable element. In the context menu, you can then select the "Evaluate" action. The evaluation dialog becomes visible with the object represented by that node as context:
The evaluation dialog is rather simple: You enter the expression you want to evaluate and press enter. The result (or possibly a syntax error or an exception) is then displayed at the bottom of the dialog. There are two possibilities to configure how the expression is evaluated:
- The "evaluation mode" slider allows you to configure the evaluation mode. By default it is "mixed". This means that expressions are evaluated based on runtime types, but without side effects. Particularly, methods are not evaluated during parsing in order to determine the methods' runtime return type because this could cause side effects.
- By clicking the settings button
you can configure many more evaluation settings.
Expression text fields have two features that simplify writing expressions:
-
Code completion: You can request code completions. The default shortcut for code completions is
Ctrl + Space
:
-
When entering method arguments, you can request the list of expected argument types. The default shortcut for this feature is
Ctrl + P
:
The evaluation settings dialog consists of three tabs:
- General Settings
- Variables
- Imports
If you have registered additional evaluation settings via EvaluationSettingsBuilder.setAdditionalSettings()
, then one additional tab per additional settings with the specified titles will be displayed.
In the "General Settings" tab you can specify the minimum access modifier fields, methods, and constructors must have in order to be accessible in expressions. Additionally, you can specify the evaluation mode:
In the "Variables" tab you can see which variables are currently defined and you can specify their type, whether they are final, and whether a hard reference to their values should be held by Marple. Additionally, you can import variables from named slots or export variables to named slots. See Debug Support for details about named and unnamed slots:
In the "Imports" tab you can manage the imported packages and classes.
Marple provides three different evaluation modes:
- static typing
- dynamic typing
- mixed typing
With static typing, expressions are evaluated based on declared types. You can only call methods and access fields that the declared types provide. When you need to analyze the runtime type, then you have to cast the instance to that type.
With dynamic typing, expressions are evaluated based on runtime types. That saves you unnecessary casts, but forces the evaluation framework to evaluate subexpressions to determine the runtime type. In particular, methods will be executed even during code completion. This can be critical if the method has side effects or if its evaluation is expensive.
Mixed typing is like dynamic typing, but without side effects. Particularly, no methods will be evaluated before the whole expression is evaluated. Instead, like with static typing the type of the value returned by a method is assumed to be the return type declared by the method.
Consider a JPanel
whose 11'th component is a JButton
:
When trying to retrieve the text from that button without dynamic typing, we get an exception that the method getText()
is unknown:
The reason is that the declared return type of
getComponent()
is Component
, which does not provide a method getText()
. Without dynamic typing, one had to cast that component to JButton
in order to call getText()
on it.
With dynamic typing activated, this cast is not required:
In this case, the parser evaluates the call to
getComponent()
and recognizes that the result is a JButton
, which provides a method getText()
.
You can introduce variables that can be access from within expressions. Variables can be preconfigured via the API and changed at runtime by the user. There are several ways to manage variables:
- Preconfiguration via API: You can create variables via the method
Variable.create()
. To set the variables, you have to retrieve the evaluator viaInspectionSettings.getEvaluator()
and assign it the variables viaExpressionEvaluator.setVariables()
. - You can select the "Add to variables" action in the context menu of a navigable element. This opens the variables dialog where you can configure the variable. You can also delete or rename variables in the variables dialog there.
- The content of the variables dialog is also shown in the "Variables" tab in the evaluation settings dialog that is opened when pressing the settings button
behind an expression input field.
The variables dialog looks as follows:
For each variable you can specify
- the variable name,
- its type,
- whether it is final, and
- whether Marple should keep a hard reference on the variable's value.
Depending on whether you declare a variable final
or not, you can use the variable as l-value in expressions (and, hence, change its value) or not. When assigning a new value to the variable, then this value must be assignable to the declared variable type. This is why you can specify this type. By default, it is set to the runtime type of the initial variable value.
When referencing variable values via hard references, you prevent the garbage collector from disposing this object. In some cases this might be what you want, in some others maybe not. This is why you can specify whether the value of a variable should be referenced by a hard reference. If not, then Marple will only hold a weak reference to the variable value and remove the variable entirely when detecting that the value has been garbage collected.
In the variables dialog you can import all named slots from the debug support as variables, overwriting all existing variables, or you can export all variables to named slots of the debug support, overwriting existing slots. The difference between variables and named slots is that variables are meant to be referenced when parsing expressions, while named slots are used to transfer data between Marple and an external debugger.
When you specify imports, then you do not have to fully qualify classes in the specified package or specified classes. Imports can be preconfigured via the API and can be changed by the user at runtime.
The snapshot dialog can be opened for JComponent
s, Image
s, Icon
s, and Paint
s via the "Snapshot" action from the context menu of a navigable element that represents it. Its primary purpose is to render it. This also works for JComponents
that are currently not visible and have not yet been rendered. Particulary for Image
s, Icon
s, and Paint
s it is helpful because it provides an easy way to see what these instances represent:
You can also save the screenshot to a file or copy it to the clipboard.
One notable option is the live preview. When activated, then the content displayed in the snapshot dialog is updated when hovering over a navigable element that represents an object that is supported by this dialog. This is helpful if you are looking for a certain component in a list of components.
Marple is a nice in-application tool for quickly analyzing objects. However, real debuggers are much more powerful and have much more sophisticated features. Nevertheless, it is sometimes not so simple to reach the object you want to analyze with your debugger. Marple can be used to find objects in such cases, either by navigating along navigable elements or by searching them in the search dialog. The debug support bridges between Marple and external debuggers to make objects known by Marple available to them or vice versa:
The debug support dialog is a graphical user interface for accessing the class DebugSupport
. It can be opened in one of the following ways:
- You can hover over a Swing component and press the debug support shortcut. The default shortcut is
Ctrl + Shift + D
. The debug support dialog becomes visible with the component as context, i.e., when entering expressions, the keywordthis
refers to that component. - You can right-click onto a navigable element and select the "Debug support" action. The debug support becomes visible with the object of the navigable element as context.
In the remainder of this section we discuss the features provided by the debug support.
The class DebugSupport
contains an array SLOTS
of 5 elements. These elements are called "unnamed slots".
Unnamed slots are easy to access from a debugger (just type, e.g., DebugSupport.SLOTS[0]
) and useful if you want to transfer a small number of instance to the debugger.
To set a value of one of these slots, you can simply double-click onto the text field and enter an expression:
As the term suggests, named slots allow you to assign instances a name under which they may be looked up. To access a slot with the name "myCustomSlot"
, call DebugSupport.getSlotValue("myCustomSlot")
in the debugger. You can also set the value of a named slot from the debugger to transfer data to Marple. The following table lists the available methods for named slots:
Method | Description |
---|---|
getSlotNames() |
Returns a Collection of the names of the currently available named slots. |
getSlotValue(slotName) |
Returns the value of the specified named slot. |
setSlotValue(slotName, value) |
Sets the specified value to the specified named slot (possibly creating it if it does not exist) if the slot name is valid. Returns true if the slot name was valid. |
renameSlot(oldSlotName, newSlotName) |
Renames the specified named slot and returns true if the renaming was successful. |
clearNamedSlots() |
Clears all named slots. |
deleteSlot(slotName) |
Deletes the named slot with the specified name. |
Named slots are a bit less convenient to use than unnamed slots, but you can use expressive names, which can be helpful if you have to transfer multiple instances between Marple and the debugger.
You can manage named slots with the "Add slot" and "Delete selected slots" buttons. You can rename named slots by simply double-clicking into their name text field. To set the value of a named slot you have to double-click into the text field and enter an expression.
Variables and named slots are very similar, but they have a different purpose: Variables are meant to be reference from within expressions, whereas named slots are meant to be accessible by external debuggers. Nevertheless, they share enough similarity that Marple provides a way to import/export variables to/from named slots. You can do so in the variables dialog, which will be opened by clicking onto the "Open variable dialog" button, or as described in the section Variables. The export feature is an easy way to share all variables you introduced in Marple with the debugger.
Custom actions can be defined at compile time as custom action settings, but can be changed by the user at runtime in the "Custom Actions" dialog. The only way to open this dialog is to press its shortcut. By default, this is Ctrl + Shift + A.
Every custom action consists of a name, an expression that will be executed when the custom action is performed, a class this action is applicable to, and optionally a keyboard shortcut. To change any of these settings, double-click onto the corresponding cell and change it as needed.
The expression will be executed in the context of a certain object that may be referenced by the literal this
. By specifying the required class you can restrict when a custom action is applicable, which also allows to write more specific expressions.
If the user has specified a preferences file via InspectionSettingsBuilder.preferencesFile()
, then these custom actions will be stored there and loaded when the next session is started.
In the search dialog you can find paths from a root instance to a concrete instance or instances of a certain class that match a user-defined search criterion:
Potential directed edges of this path exist
- from an instance to any of its fields,
- from
Iterable
s and arrays to their elements, and - from
Map
s andMultimap
s to their keys and values.
You can control which edge types and paths are considered by the following options:
- non-static fields: If selected, then edges from instances to static fields of their classes are ignored.
- pure fields only: If selected, then only edges from instances to their fields are considered.
- limit search depths: If selected, then paths that are longer than the specified length are ignored.
There are two ways to open the search dialog:
- You can hover over a Swing component and press the instance search shortcut. The default shortcut is
Ctrl + Shift + F
. The search dialog becomes visible with the component as root of the search. - You can right-click onto a navigable element and select the search instances from here or the search this instance action. In both cases the search dialog becomes visible. The first action sets the object of the navigable element as search root, the second action sets it as target instance.
Since you have to set root and target, you will often use both actions one after another. This is possible since this dialog is, like many other Marple dialogs, non-modal.
Searching instances may be helpful for analyzing memory leaks: If you already know that a certain instance has not been disposed, which instance keeps a reference to it and why.
Marple utilizes the following open source projects:
https://github.com/bobbylight/autocomplete
Copyright (c) 2012, Robert Futrell.
AutoComplete is licensed under the Modified BSD license.
https://github.com/google/guava
Guava is licensed under the Apache License 2.0.
https://github.com/tbrunsch/Zenodot
Zenodot is licensed under the MIT License.