Skip to content

Commit ba3e444

Browse files
Petr Hlubu?ekfacebook-github-bot
authored andcommitted
Properties pane added
Summary: A properties-like grid pane was added to enable setting/changing values in python/lua backend. * A `PropertiesPane` was added, displayed using `vis.properties()` * New `PropertyUpdate` event added to propagate events to backend * Key press events interfered with textbox input in property pane, so I was forced to move the key events from main canvas to `TextPane`. Main canvas still catches key events, but just publishes them to newly added `EventSystem` (simple event queue implementation). `TextPane` subscribes to the queue and thru `appApi` sends the event as was before. * `appApi` was added to props of Panes containing just `sendPaneMessage` - a message from the pane that will be forwarded to backend (with the evironment and pane id added) * CSS styling of focused pane changed - now just a widow header bar (title) is higlighted on focused window, not window content (it was ugly in properties pane and) Example of usage is in `demo-properties.py`. It shows that even a backend validation can be done (when `PropertyUpdate` event is received, the `viz.properties()` can be called again with previous or updated values. Closes #361 Differential Revision: D8162524 Pulled By: JackUrb fbshipit-source-id: 80480377f178456701557fc87907a68b46fda3ea
1 parent d5dc9fb commit ba3e444

File tree

12 files changed

+552
-39
lines changed

12 files changed

+552
-39
lines changed

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,15 @@ You can subscribe a window to events by adding a function to the event handlers
5858

5959
Additional parameters are defined below.
6060

61-
Right now two callback events are supported:
61+
Right now three callback events are supported:
6262

6363
1. `Close` - Triggers when a window is closed. Returns a dict with only the aforementioned fields.
6464
2. `KeyPress` - Triggers when a key is pressed. Contains additional parameters:
6565
- `key` - A string representation of the key pressed (applying state modifiers such as SHIFT)
6666
- `key_code` - The javascript event keycode for the pressed key (no modifiers)
67+
3. `PropertyUpdate` - Triggers when a property is updated in Property pane
68+
- `propertyId` - Position in properties list
69+
- `value` - New property value
6770

6871
### Environments
6972
<p align="center"><img align="center" src="https://user-images.githubusercontent.com/1276867/34618198-fc63976c-f20b-11e7-9c0d-060132fdb37e.png" width="300" /></p>
@@ -229,6 +232,7 @@ Visdom offers the following basic visualization functions:
229232
- [`vis.image`](#visimage) : image
230233
- [`vis.images`](#visimages) : list of images
231234
- [`vis.text`](#vistext) : arbitrary HTML
235+
- [`vis.properties`](#visproperties) : properties grid
232236
- [`vis.audio`](#visaudio) : audio
233237
- [`vis.video`](#visvideo) : videos
234238
- [`vis.svg`](#vissvg) : SVG object
@@ -302,6 +306,33 @@ This function prints text in a box. You can use this to embed arbitrary HTML.
302306
It takes as input a `text` string.
303307
No specific `opts` are currently supported.
304308

309+
#### vis.properties
310+
This function shows editable properties in a pane. Properties are expected to be a List of Dicts e.g.:
311+
```
312+
properties = [
313+
{'type': 'text', 'name': 'Text input', 'value': 'initial'},
314+
{'type': 'number', 'name': 'Number input', 'value': '12'},
315+
{'type': 'button', 'name': 'Button', 'value': 'Start'},
316+
{'type': 'checkbox', 'name': 'Checkbox', 'value': True},
317+
{'type': 'select', 'name': 'Select', 'value': 1, 'values': ['Red', 'Green', 'Blue']},
318+
]
319+
```
320+
Supported types:
321+
- text: string
322+
- number: decimal number
323+
- button: button labeled with "value"
324+
- checkbox: boolean value rendered as a checkbox
325+
- select: multiple values select box
326+
- `value`: id of selected value (zero based)
327+
- `values`: list of possible values
328+
329+
Callback are called on property value update:
330+
- `event_type`: `"PropertyUpdate"`
331+
- `propertyId`: position in the `properties` list
332+
- `value`: new value
333+
334+
No specific `opts` are currently supported.
335+
305336
#### vis.audio
306337
This function plays audio. It takes as input the filename of the audio
307338
file or an `N` tensor containing the waveform (use an `Nx2` matrix for stereo

example/demo-properties.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright 2017-present, Facebook, Inc.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
from __future__ import absolute_import
8+
from __future__ import division
9+
from __future__ import print_function
10+
from __future__ import unicode_literals
11+
12+
from visdom import Visdom
13+
import time
14+
import numpy as np
15+
16+
try:
17+
viz = Visdom()
18+
19+
startup_sec = 1
20+
while not viz.check_connection() and startup_sec > 0:
21+
time.sleep(0.1)
22+
startup_sec -= 0.1
23+
assert viz.check_connection(), 'No connection could be formed quickly'
24+
25+
# image callback demo
26+
def show_color_image_window(color, win=None):
27+
image = np.full([3, 256, 256], color, dtype=float)
28+
return viz.image(
29+
image,
30+
opts=dict(title='Colors', caption='Press arrows to alter color.'),
31+
win=win
32+
)
33+
34+
image_color = 0
35+
callback_image_window = show_color_image_window(image_color)
36+
37+
def image_callback(event):
38+
global image_color
39+
if event['event_type'] == 'KeyPress':
40+
if event['key'] == 'ArrowRight':
41+
image_color = min(image_color + 0.2, 1)
42+
if event['key'] == 'ArrowLeft':
43+
image_color = max(image_color - 0.2, 0)
44+
show_color_image_window(image_color, callback_image_window)
45+
46+
viz.register_event_handler(image_callback, callback_image_window)
47+
48+
# text window with Callbacks
49+
txt = 'This is a write demo notepad. Type below. Delete clears text:<br>'
50+
callback_text_window = viz.text(txt)
51+
52+
def type_callback(event):
53+
if event['event_type'] == 'KeyPress':
54+
curr_txt = event['pane_data']['content']
55+
if event['key'] == 'Enter':
56+
curr_txt += '<br>'
57+
elif event['key'] == 'Backspace':
58+
curr_txt = curr_txt[:-1]
59+
elif event['key'] == 'Delete':
60+
curr_txt = txt
61+
elif len(event['key']) == 1:
62+
curr_txt += event['key']
63+
viz.text(curr_txt, win=callback_text_window)
64+
65+
viz.register_event_handler(type_callback, callback_text_window)
66+
67+
# Properties window
68+
properties = [
69+
{'type': 'text', 'name': 'Text input', 'value': 'initial'},
70+
{'type': 'number', 'name': 'Number input', 'value': '12'},
71+
{'type': 'button', 'name': 'Button', 'value': 'Start'},
72+
{'type': 'checkbox', 'name': 'Checkbox', 'value': True},
73+
{'type': 'select', 'name': 'Select', 'value': 1, 'values': ['Red', 'Green', 'Blue']},
74+
]
75+
76+
properties_window = viz.properties(properties)
77+
78+
def properties_callback(event):
79+
if event['event_type'] == 'PropertyUpdate':
80+
prop_id = event['propertyId']
81+
value = event['value']
82+
if prop_id == 0:
83+
new_value = value + '_updated'
84+
elif prop_id == 1:
85+
new_value = value + '0'
86+
elif prop_id == 2:
87+
new_value = 'Stop' if properties[prop_id]['value'] == 'Start' else 'Start'
88+
else:
89+
new_value = value
90+
properties[prop_id]['value'] = new_value
91+
viz.properties(properties, win=properties_window)
92+
viz.text("Updated: {} => {}".format(properties[event['propertyId']]['name'], str(event['value'])),
93+
win=callback_text_window, append=True)
94+
95+
viz.register_event_handler(properties_callback, properties_window)
96+
97+
try:
98+
input = raw_input # for Python 2 compatibility
99+
except NameError:
100+
pass
101+
input('Waiting for callbacks, press enter to quit.')
102+
except BaseException as e:
103+
print(
104+
"The visdom experienced an exception while running: {}\n"
105+
"The demo displays up-to-date functionality with the GitHub version, "
106+
"which may not yet be pushed to pip. Please upgrade using "
107+
"`pip install -e .` or `easy_install .`\n"
108+
"If this does not resolve the problem, please open an issue on "
109+
"our GitHub.".format(repr(e))
110+
)

js/EventSystem.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright 2017-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
*/
9+
10+
class EventSystem {
11+
constructor() {
12+
this.queue = {};
13+
}
14+
15+
publish(event, data) {
16+
let queue = this.queue[event];
17+
18+
if (typeof queue === 'undefined') {
19+
return false;
20+
}
21+
22+
queue.forEach((cb) => cb(data));
23+
24+
return true;
25+
}
26+
27+
subscribe(event, callback) {
28+
if (typeof this.queue[event] === 'undefined') {
29+
this.queue[event] = [];
30+
}
31+
32+
this.queue[event].push(callback);
33+
}
34+
35+
// the callback parameter is optional. Without it the whole event will be
36+
// removed, instead of just one subscibtion. Fine for simple implementation
37+
unsubscribe(event, callback) {
38+
let queue = this.queue;
39+
40+
if (typeof queue[event] !== 'undefined') {
41+
if (typeof callback === 'undefined') {
42+
delete queue[event];
43+
} else {
44+
this.queue[event] = queue[event].filter(function(sub) {
45+
return sub !== callback;
46+
})
47+
}
48+
}
49+
}
50+
}
51+
52+
module.exports = new EventSystem();

js/ImagePane.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*
88
*/
99

10+
import EventSystem from "./EventSystem";
1011
const Pane = require('./Pane');
1112

1213
class ImagePane extends React.Component {
@@ -18,6 +19,35 @@ class ImagePane extends React.Component {
1819
ty: 0.,
1920
}
2021

22+
onEvent = (e) => {
23+
if( !this.props.isFocused ) {
24+
return;
25+
}
26+
27+
switch(e.type) {
28+
case 'keydown':
29+
case 'keypress':
30+
e.preventDefault();
31+
break;
32+
case 'keyup':
33+
this.props.appApi.sendPaneMessage(
34+
{
35+
event_type: 'KeyPress',
36+
key: event.key,
37+
key_code: event.keyCode,
38+
}
39+
);
40+
break;
41+
}
42+
};
43+
44+
componentDidMount() {
45+
EventSystem.subscribe('global.event', this.onEvent)
46+
}
47+
componentWillMount() {
48+
EventSystem.unsubscribe('global.event', this.onEvent)
49+
}
50+
2151
handleDownload = () => {
2252
var link = document.createElement('a');
2353
link.download = `${this.props.title || 'visdom_image'}.jpg`;

js/PropertiesPane.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Copyright 2017-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
*/
9+
10+
const Pane = require('./Pane');
11+
12+
13+
class Text extends React.Component {
14+
constructor(props) {
15+
super(props);
16+
this.state = {value: props.value};
17+
}
18+
19+
handleChange = (event) => {
20+
let newValue = event.target.value;
21+
if( this.props.validateHandler && !this.props.validateHandler(newValue)) {
22+
event.preventDefault();
23+
} else {
24+
this.setState({value: newValue});
25+
}
26+
};
27+
28+
handleKeyPress = (event) => {
29+
if( event.key === "Enter") {
30+
if( this.props.submitHandler ) {
31+
this.props.submitHandler(this.state.value);
32+
}
33+
}
34+
};
35+
36+
componentWillReceiveProps(nextProps) {
37+
if( this.state.value !== nextProps.value) {
38+
this.setState({value: nextProps.value});
39+
}
40+
}
41+
42+
render() {
43+
return (
44+
<input type="text" value={this.state.value} onChange={this.handleChange} onKeyPress={this.handleKeyPress}/>
45+
);
46+
}
47+
}
48+
49+
class PropertiesPane extends React.Component {
50+
51+
handleDownload = () => {
52+
var blob = new Blob([JSON.stringify(this.props.content)], {type:"application/json"});
53+
var url = window.URL.createObjectURL(blob);
54+
var link = document.createElement("a");
55+
link.download = 'visdom_properties.json';
56+
link.href = url;
57+
link.click();
58+
};
59+
60+
updateValue = (propId, value) => {
61+
this.props.onFocus(this.props.id, () => {
62+
this.props.appApi.sendPaneMessage(
63+
{
64+
event_type: 'PropertyUpdate',
65+
propertyId: propId,
66+
value: value
67+
}
68+
);
69+
});
70+
};
71+
72+
renderPropertyValue = (prop, propId) => {
73+
switch(prop.type) {
74+
case 'text':
75+
return <Text
76+
value={prop.value}
77+
submitHandler={(value) => this.updateValue(propId, value)}
78+
/>;
79+
case 'number':
80+
return <Text
81+
value={prop.value}
82+
submitHandler={(value) => this.updateValue(propId, value)}
83+
validateHandler={(value) => value.match(/^[0-9]*([.][0-9]*)?$/i)}
84+
/>;
85+
case 'button':
86+
return <button
87+
className="btn btn-sm"
88+
onClick={() => this.updateValue(propId, "clicked")}
89+
>{prop.value}</button>
90+
case 'checkbox':
91+
return <label className="checkbox-inline">
92+
<input
93+
type="checkbox"
94+
checked={prop.value}
95+
onChange={() => this.updateValue(propId, !prop.value)}
96+
/>
97+
&nbsp;
98+
</label>;
99+
case 'select':
100+
return <select className="form-control"
101+
onChange={(event) => this.updateValue(propId, event.target.value)}
102+
value={prop.value}
103+
>
104+
{prop.values.map((name, id) => <option value={id}>{name}</option>)}
105+
</select>;
106+
}
107+
};
108+
109+
render() {
110+
return (
111+
<Pane {...this.props} handleDownload={this.handleDownload}>
112+
<div className="content-properties">
113+
<table className="table table-bordered table-condensed table-properties">
114+
{this.props.content.map((prop, propId) =>
115+
<tr key={propId}>
116+
<td className="table-properties-name">{prop.name}</td>
117+
<td className="table-properties-value">{this.renderPropertyValue(prop, propId)}</td>
118+
</tr>
119+
)}
120+
</table>
121+
</div>
122+
</Pane>
123+
)
124+
}
125+
}
126+
127+
module.exports = PropertiesPane;

0 commit comments

Comments
 (0)