mirror of
https://github.com/python/cpython.git
synced 2025-11-12 07:02:33 +00:00
Moved stop button again; default form position next to video window.
This commit is contained in:
parent
bc6d3c37d1
commit
c5a14331a0
2 changed files with 178 additions and 117 deletions
|
|
@ -2,13 +2,20 @@
|
||||||
|
|
||||||
# Video bag-of-tricks
|
# Video bag-of-tricks
|
||||||
|
|
||||||
# XXX To do: audio; rationalize user interface; ...?
|
# XXX To do:
|
||||||
|
# - audio
|
||||||
|
# - single frame recording
|
||||||
|
# - improve user interface
|
||||||
|
# - help button?
|
||||||
|
# - command line options to set initial settings
|
||||||
|
# - save settings in a file
|
||||||
|
# - ...?
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import getopt
|
import getopt
|
||||||
import string
|
import string
|
||||||
import os
|
import os
|
||||||
sts = os.system('makemap') # Must be before "import fl"
|
sts = os.system('makemap') # Must be before "import fl" to work
|
||||||
import sgi
|
import sgi
|
||||||
import gl
|
import gl
|
||||||
import GL
|
import GL
|
||||||
|
|
@ -28,6 +35,7 @@ WATCH = 1
|
||||||
watchcursor.defwatch(WATCH)
|
watchcursor.defwatch(WATCH)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
## fl.set_graphics_mode(0, 1)
|
||||||
vb = VideoBagOfTricks().init()
|
vb = VideoBagOfTricks().init()
|
||||||
while 1:
|
while 1:
|
||||||
dummy = fl.do_forms()
|
dummy = fl.do_forms()
|
||||||
|
|
@ -46,16 +54,31 @@ class VideoBagOfTricks:
|
||||||
formdef = flp.parse_form('VbForm', 'form')
|
formdef = flp.parse_form('VbForm', 'form')
|
||||||
flp.create_full_form(self, formdef)
|
flp.create_full_form(self, formdef)
|
||||||
self.g_stop.hide_object()
|
self.g_stop.hide_object()
|
||||||
|
self.g_burst.hide_object()
|
||||||
self.setdefaults()
|
self.setdefaults()
|
||||||
self.openvideo()
|
self.openvideo()
|
||||||
self.makewindow()
|
self.makewindow()
|
||||||
self.bindvideo()
|
self.bindvideo()
|
||||||
self.capturing = 0
|
self.capturing = 0
|
||||||
self.form.show_form(FL.PLACE_SIZE, FL.TRUE, \
|
self.showform()
|
||||||
'Video Bag Of Tricks')
|
|
||||||
fl.set_event_call_back(self.do_event)
|
fl.set_event_call_back(self.do_event)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def showform(self):
|
||||||
|
# Get position of video window
|
||||||
|
gl.winset(self.window)
|
||||||
|
x, y = gl.getorigin()
|
||||||
|
width, height = gl.getsize()
|
||||||
|
# Calculate position of form window
|
||||||
|
x1 = x + width + 10
|
||||||
|
x2 = x1 + int(self.form.w) - 1
|
||||||
|
y2 = y + height - 1
|
||||||
|
y1 = y2 - int(self.form.h) + 1
|
||||||
|
# Position and show form window
|
||||||
|
gl.prefposition(x1, x2, y1, y2)
|
||||||
|
self.form.show_form(FL.PLACE_FREE, FL.TRUE, \
|
||||||
|
'Video Bag Of Tricks')
|
||||||
|
|
||||||
def setdefaults(self):
|
def setdefaults(self):
|
||||||
self.mono_thresh = 128
|
self.mono_thresh = 128
|
||||||
self.format = 'rgb8'
|
self.format = 'rgb8'
|
||||||
|
|
@ -152,34 +175,6 @@ class VideoBagOfTricks:
|
||||||
self.rebindvideo()
|
self.rebindvideo()
|
||||||
self.settitle()
|
self.settitle()
|
||||||
|
|
||||||
def get_format(self):
|
|
||||||
i = self.c_format.get_choice()
|
|
||||||
label = Labels[i-1]
|
|
||||||
format = Formats[i-1]
|
|
||||||
self.format = format
|
|
||||||
#
|
|
||||||
self.rgb = (format[:3] == 'rgb')
|
|
||||||
self.mono = (format == 'mono')
|
|
||||||
self.grey = (format[:4] == 'grey')
|
|
||||||
self.mono_use_thresh = (label == 'mono thresh')
|
|
||||||
s = format[4:]
|
|
||||||
if s:
|
|
||||||
self.greybits = string.atoi(s)
|
|
||||||
else:
|
|
||||||
self.greybits = 8
|
|
||||||
if label == 'grey2 dith':
|
|
||||||
self.greybits = -2
|
|
||||||
#
|
|
||||||
convertor = None
|
|
||||||
if self.grey:
|
|
||||||
if self.greybits == 2:
|
|
||||||
convertor = imageop.grey2grey2
|
|
||||||
elif self.greybits == 4:
|
|
||||||
convertor = imageop.grey2grey4
|
|
||||||
elif self.greybits == -2:
|
|
||||||
convertor = imageop.dither2grey2
|
|
||||||
self.convertor = convertor
|
|
||||||
|
|
||||||
def cb_format(self, *args):
|
def cb_format(self, *args):
|
||||||
self.get_format()
|
self.get_format()
|
||||||
if self.mono_use_thresh:
|
if self.mono_use_thresh:
|
||||||
|
|
@ -200,7 +195,16 @@ class VideoBagOfTricks:
|
||||||
self.rebindvideo()
|
self.rebindvideo()
|
||||||
|
|
||||||
def cb_burst(self, *args):
|
def cb_burst(self, *args):
|
||||||
pass
|
if self.b_burst.get_button():
|
||||||
|
self.in_rate.set_input('1')
|
||||||
|
self.b_drop.set_button(1)
|
||||||
|
## self.g_stop.hide_object()
|
||||||
|
self.g_burst.show_object()
|
||||||
|
else:
|
||||||
|
self.in_rate.set_input('2')
|
||||||
|
self.b_drop.set_button(0)
|
||||||
|
## self.g_stop.show_object()
|
||||||
|
self.g_burst.hide_object()
|
||||||
|
|
||||||
def cb_maxmem(self, *args):
|
def cb_maxmem(self, *args):
|
||||||
pass
|
pass
|
||||||
|
|
@ -226,15 +230,6 @@ class VideoBagOfTricks:
|
||||||
self.in_file.set_input(filename)
|
self.in_file.set_input(filename)
|
||||||
self.cb_file()
|
self.cb_file()
|
||||||
|
|
||||||
def cb_play(self, *args):
|
|
||||||
filename = self.in_file.get_input()
|
|
||||||
sts = os.system('Vplay -q ' + filename + ' &')
|
|
||||||
|
|
||||||
def cb_stop(self, *args):
|
|
||||||
if self.capturing:
|
|
||||||
raise StopCapture
|
|
||||||
gl.ringbell()
|
|
||||||
|
|
||||||
def cb_capture(self, *args):
|
def cb_capture(self, *args):
|
||||||
if not self.video:
|
if not self.video:
|
||||||
gl.ringbell()
|
gl.ringbell()
|
||||||
|
|
@ -244,6 +239,18 @@ class VideoBagOfTricks:
|
||||||
else:
|
else:
|
||||||
self.cont_capture()
|
self.cont_capture()
|
||||||
|
|
||||||
|
def cb_stop(self, *args):
|
||||||
|
if self.capturing:
|
||||||
|
raise StopCapture
|
||||||
|
gl.ringbell()
|
||||||
|
|
||||||
|
def cb_play(self, *args):
|
||||||
|
filename = self.in_file.get_input()
|
||||||
|
sts = os.system('Vplay -q ' + filename + ' &')
|
||||||
|
|
||||||
|
def cb_quit(self, *args):
|
||||||
|
raise SystemExit, 0
|
||||||
|
|
||||||
def burst_capture(self):
|
def burst_capture(self):
|
||||||
self.setwatch()
|
self.setwatch()
|
||||||
gl.winset(self.window)
|
gl.winset(self.window)
|
||||||
|
|
@ -264,7 +271,6 @@ class VideoBagOfTricks:
|
||||||
self.mono or self.grey, memsize)
|
self.mono or self.grey, memsize)
|
||||||
print 'nframes =', nframes
|
print 'nframes =', nframes
|
||||||
rate = string.atoi(self.in_rate.get_input())
|
rate = string.atoi(self.in_rate.get_input())
|
||||||
# XXX Should check ranges and not crash if non-integer
|
|
||||||
info = (vformat, x, y, nframes, rate)
|
info = (vformat, x, y, nframes, rate)
|
||||||
try:
|
try:
|
||||||
info2, data, bitvec = self.video.CaptureBurst(info)
|
info2, data, bitvec = self.video.CaptureBurst(info)
|
||||||
|
|
@ -272,7 +278,7 @@ class VideoBagOfTricks:
|
||||||
fl.show_message('Capture error:', str(msg), '')
|
fl.show_message('Capture error:', str(msg), '')
|
||||||
self.setarrow()
|
self.setarrow()
|
||||||
return
|
return
|
||||||
print info2
|
if info <> info2: print info, '<>', info2
|
||||||
self.save_burst(info2, data, bitvec)
|
self.save_burst(info2, data, bitvec)
|
||||||
self.setarrow()
|
self.setarrow()
|
||||||
|
|
||||||
|
|
@ -361,6 +367,34 @@ class VideoBagOfTricks:
|
||||||
self.g_main.show_object()
|
self.g_main.show_object()
|
||||||
self.setarrow()
|
self.setarrow()
|
||||||
|
|
||||||
|
def get_format(self):
|
||||||
|
i = self.c_format.get_choice()
|
||||||
|
label = Labels[i-1]
|
||||||
|
format = Formats[i-1]
|
||||||
|
self.format = format
|
||||||
|
#
|
||||||
|
self.rgb = (format[:3] == 'rgb')
|
||||||
|
self.mono = (format == 'mono')
|
||||||
|
self.grey = (format[:4] == 'grey')
|
||||||
|
self.mono_use_thresh = (label == 'mono thresh')
|
||||||
|
s = format[4:]
|
||||||
|
if s:
|
||||||
|
self.greybits = string.atoi(s)
|
||||||
|
else:
|
||||||
|
self.greybits = 8
|
||||||
|
if label == 'grey2 dith':
|
||||||
|
self.greybits = -2
|
||||||
|
#
|
||||||
|
convertor = None
|
||||||
|
if self.grey:
|
||||||
|
if self.greybits == 2:
|
||||||
|
convertor = imageop.grey2grey2
|
||||||
|
elif self.greybits == 4:
|
||||||
|
convertor = imageop.grey2grey4
|
||||||
|
elif self.greybits == -2:
|
||||||
|
convertor = imageop.dither2grey2
|
||||||
|
self.convertor = convertor
|
||||||
|
|
||||||
def open_file(self):
|
def open_file(self):
|
||||||
gl.winset(self.window)
|
gl.winset(self.window)
|
||||||
x, y = gl.getsize()
|
x, y = gl.getsize()
|
||||||
|
|
@ -374,15 +408,6 @@ class VideoBagOfTricks:
|
||||||
vout.writeheader()
|
vout.writeheader()
|
||||||
self.vout = vout
|
self.vout = vout
|
||||||
|
|
||||||
def close_file(self):
|
|
||||||
try:
|
|
||||||
self.vout.close()
|
|
||||||
except IOError, msg:
|
|
||||||
if msg == (0, 'Error 0'):
|
|
||||||
msg = 'disk full??'
|
|
||||||
fl.show_message('IOError', str(msg), '')
|
|
||||||
del self.vout
|
|
||||||
|
|
||||||
def write_frame(self, t, data):
|
def write_frame(self, t, data):
|
||||||
if self.convertor:
|
if self.convertor:
|
||||||
data = self.convertor(data, len(data), 1)
|
data = self.convertor(data, len(data), 1)
|
||||||
|
|
@ -403,8 +428,14 @@ class VideoBagOfTricks:
|
||||||
return 0
|
return 0
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def cb_quit(self, *args):
|
def close_file(self):
|
||||||
raise SystemExit, 0
|
try:
|
||||||
|
self.vout.close()
|
||||||
|
except IOError, msg:
|
||||||
|
if msg == (0, 'Error 0'):
|
||||||
|
msg = 'disk full??'
|
||||||
|
fl.show_message('IOError', str(msg), '')
|
||||||
|
del self.vout
|
||||||
|
|
||||||
def setwatch(self):
|
def setwatch(self):
|
||||||
gl.winset(self.form.window)
|
gl.winset(self.form.window)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Number of forms: 1
|
||||||
Name: form
|
Name: form
|
||||||
Width: 350.000000
|
Width: 350.000000
|
||||||
Height: 240.000000
|
Height: 240.000000
|
||||||
Number of Objects: 21
|
Number of Objects: 23
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
class: 1
|
class: 1
|
||||||
|
|
@ -31,61 +31,31 @@ class: 10000
|
||||||
type: 0
|
type: 0
|
||||||
box: 0.000000 0.000000 0.000000 0.000000
|
box: 0.000000 0.000000 0.000000 0.000000
|
||||||
boxtype: 0
|
boxtype: 0
|
||||||
colors: 0 0
|
colors: 5487 512
|
||||||
alignment: 4
|
alignment: 4
|
||||||
style: 0
|
style: 0
|
||||||
size: 11.000000
|
size: 11.000000
|
||||||
lcol: 0
|
lcol: 0
|
||||||
label:
|
label:
|
||||||
name: g_main
|
name: g_burst
|
||||||
callback:
|
callback:
|
||||||
argument:
|
argument:
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
class: 1
|
class: 1
|
||||||
type: 2
|
type: 1
|
||||||
box: 140.000000 10.000000 120.000000 160.000000
|
box: 140.000000 10.000000 120.000000 120.000000
|
||||||
boxtype: 2
|
boxtype: 1
|
||||||
colors: 47 47
|
colors: 47 47
|
||||||
alignment: 0
|
alignment: 0
|
||||||
style: 0
|
style: 0
|
||||||
size: 11.000000
|
size: 11.000000
|
||||||
lcol: 0
|
lcol: 0
|
||||||
label: Burst mode:
|
label:
|
||||||
name:
|
name:
|
||||||
callback:
|
callback:
|
||||||
argument:
|
argument:
|
||||||
|
|
||||||
--------------------
|
|
||||||
class: 1
|
|
||||||
type: 2
|
|
||||||
box: 10.000000 10.000000 120.000000 160.000000
|
|
||||||
boxtype: 2
|
|
||||||
colors: 47 47
|
|
||||||
alignment: 0
|
|
||||||
style: 0
|
|
||||||
size: 11.000000
|
|
||||||
lcol: 0
|
|
||||||
label: Options:
|
|
||||||
name:
|
|
||||||
callback:
|
|
||||||
argument:
|
|
||||||
|
|
||||||
--------------------
|
|
||||||
class: 31
|
|
||||||
type: 2
|
|
||||||
box: 60.000000 50.000000 40.000000 30.000000
|
|
||||||
boxtype: 2
|
|
||||||
colors: 13 5
|
|
||||||
alignment: 0
|
|
||||||
style: 0
|
|
||||||
size: 11.000000
|
|
||||||
lcol: 0
|
|
||||||
label: Capture rate:
|
|
||||||
name: in_rate
|
|
||||||
callback: cb_rate
|
|
||||||
argument: 0
|
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
class: 31
|
class: 31
|
||||||
type: 1
|
type: 1
|
||||||
|
|
@ -116,10 +86,70 @@ name: in_nframes
|
||||||
callback: cb_nframes
|
callback: cb_nframes
|
||||||
argument: 0
|
argument: 0
|
||||||
|
|
||||||
|
--------------------
|
||||||
|
class: 20000
|
||||||
|
type: 0
|
||||||
|
box: 0.000000 0.000000 0.000000 0.000000
|
||||||
|
boxtype: 0
|
||||||
|
colors: -322390740 -895472437
|
||||||
|
alignment: 4
|
||||||
|
style: 0
|
||||||
|
size: 11.000000
|
||||||
|
lcol: 0
|
||||||
|
label:
|
||||||
|
name:
|
||||||
|
callback:
|
||||||
|
argument:
|
||||||
|
|
||||||
|
--------------------
|
||||||
|
class: 10000
|
||||||
|
type: 0
|
||||||
|
box: 0.000000 0.000000 0.000000 0.000000
|
||||||
|
boxtype: 0
|
||||||
|
colors: 1147496041 1852404841
|
||||||
|
alignment: 4
|
||||||
|
style: 0
|
||||||
|
size: 11.000000
|
||||||
|
lcol: 0
|
||||||
|
label:
|
||||||
|
name: g_main
|
||||||
|
callback:
|
||||||
|
argument:
|
||||||
|
|
||||||
|
--------------------
|
||||||
|
class: 1
|
||||||
|
type: 1
|
||||||
|
box: 10.000000 10.000000 120.000000 120.000000
|
||||||
|
boxtype: 1
|
||||||
|
colors: 47 47
|
||||||
|
alignment: 0
|
||||||
|
style: 0
|
||||||
|
size: 11.000000
|
||||||
|
lcol: 0
|
||||||
|
label:
|
||||||
|
name:
|
||||||
|
callback:
|
||||||
|
argument:
|
||||||
|
|
||||||
|
--------------------
|
||||||
|
class: 31
|
||||||
|
type: 2
|
||||||
|
box: 50.000000 70.000000 40.000000 30.000000
|
||||||
|
boxtype: 2
|
||||||
|
colors: 13 5
|
||||||
|
alignment: 0
|
||||||
|
style: 0
|
||||||
|
size: 11.000000
|
||||||
|
lcol: 0
|
||||||
|
label: Capture rate:
|
||||||
|
name: in_rate
|
||||||
|
callback: cb_rate
|
||||||
|
argument: 0
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
class: 12
|
class: 12
|
||||||
type: 1
|
type: 1
|
||||||
box: 150.000000 130.000000 100.000000 30.000000
|
box: 140.000000 140.000000 120.000000 30.000000
|
||||||
boxtype: 1
|
boxtype: 1
|
||||||
colors: 39 3
|
colors: 39 3
|
||||||
alignment: 4
|
alignment: 4
|
||||||
|
|
@ -164,7 +194,7 @@ argument: 0
|
||||||
--------------------
|
--------------------
|
||||||
class: 11
|
class: 11
|
||||||
type: 0
|
type: 0
|
||||||
box: 270.000000 110.000000 70.000015 60.000004
|
box: 270.000000 140.000000 70.000015 30.000002
|
||||||
boxtype: 1
|
boxtype: 1
|
||||||
colors: 47 47
|
colors: 47 47
|
||||||
alignment: 4
|
alignment: 4
|
||||||
|
|
@ -176,25 +206,10 @@ name: b_capture
|
||||||
callback: cb_capture
|
callback: cb_capture
|
||||||
argument: 0
|
argument: 0
|
||||||
|
|
||||||
--------------------
|
|
||||||
class: 13
|
|
||||||
type: 1
|
|
||||||
box: 20.000000 20.000000 110.000000 30.000000
|
|
||||||
boxtype: 0
|
|
||||||
colors: 7 3
|
|
||||||
alignment: 4
|
|
||||||
style: 0
|
|
||||||
size: 11.000000
|
|
||||||
lcol: 0
|
|
||||||
label: "Fielddrop"
|
|
||||||
name: b_drop
|
|
||||||
callback: cb_drop
|
|
||||||
argument: 0
|
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
class: 2
|
class: 2
|
||||||
type: 0
|
type: 0
|
||||||
box: 30.000000 50.000000 30.000000 30.000000
|
box: 20.000000 70.000000 30.000000 30.000000
|
||||||
boxtype: 0
|
boxtype: 0
|
||||||
colors: 47 47
|
colors: 47 47
|
||||||
alignment: 2
|
alignment: 2
|
||||||
|
|
@ -209,7 +224,7 @@ argument:
|
||||||
--------------------
|
--------------------
|
||||||
class: 2
|
class: 2
|
||||||
type: 0
|
type: 0
|
||||||
box: 100.000000 50.000000 30.000000 30.000000
|
box: 90.000000 70.000000 30.000000 30.000000
|
||||||
boxtype: 0
|
boxtype: 0
|
||||||
colors: 47 47
|
colors: 47 47
|
||||||
alignment: 2
|
alignment: 2
|
||||||
|
|
@ -254,7 +269,7 @@ argument: 0
|
||||||
--------------------
|
--------------------
|
||||||
class: 42
|
class: 42
|
||||||
type: 0
|
type: 0
|
||||||
box: 20.000000 110.000000 100.000000 30.000000
|
box: 10.000000 140.000000 120.000000 30.000000
|
||||||
boxtype: 5
|
boxtype: 5
|
||||||
colors: 7 0
|
colors: 7 0
|
||||||
alignment: 0
|
alignment: 0
|
||||||
|
|
@ -266,12 +281,27 @@ name: c_format
|
||||||
callback: cb_format
|
callback: cb_format
|
||||||
argument: 0
|
argument: 0
|
||||||
|
|
||||||
|
--------------------
|
||||||
|
class: 12
|
||||||
|
type: 1
|
||||||
|
box: 20.000000 20.000000 100.000000 30.000000
|
||||||
|
boxtype: 1
|
||||||
|
colors: 39 3
|
||||||
|
alignment: 4
|
||||||
|
style: 0
|
||||||
|
size: 11.000000
|
||||||
|
lcol: 0
|
||||||
|
label: Fielddrop
|
||||||
|
name: b_drop
|
||||||
|
callback: cb_drop
|
||||||
|
argument: 0
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
class: 20000
|
class: 20000
|
||||||
type: 0
|
type: 0
|
||||||
box: 0.000000 0.000000 0.000000 0.000000
|
box: 0.000000 0.000000 0.000000 0.000000
|
||||||
boxtype: 0
|
boxtype: 0
|
||||||
colors: 0 0
|
colors: 544171552 1331849829
|
||||||
alignment: 4
|
alignment: 4
|
||||||
style: 0
|
style: 0
|
||||||
size: 11.000000
|
size: 11.000000
|
||||||
|
|
@ -286,7 +316,7 @@ class: 10000
|
||||||
type: 0
|
type: 0
|
||||||
box: 0.000000 0.000000 0.000000 0.000000
|
box: 0.000000 0.000000 0.000000 0.000000
|
||||||
boxtype: 0
|
boxtype: 0
|
||||||
colors: 0 0
|
colors: 1147496041 1852404841
|
||||||
alignment: 4
|
alignment: 4
|
||||||
style: 0
|
style: 0
|
||||||
size: 11.000000
|
size: 11.000000
|
||||||
|
|
@ -299,14 +329,14 @@ argument:
|
||||||
--------------------
|
--------------------
|
||||||
class: 11
|
class: 11
|
||||||
type: 0
|
type: 0
|
||||||
box: 10.000000 10.000000 330.000000 220.000000
|
box: 270.000000 140.000000 70.000000 30.000000
|
||||||
boxtype: 1
|
boxtype: 1
|
||||||
colors: 47 47
|
colors: 47 47
|
||||||
alignment: 4
|
alignment: 4
|
||||||
style: 0
|
style: 0
|
||||||
size: 11.000000
|
size: 11.000000
|
||||||
lcol: 0
|
lcol: 0
|
||||||
label: Stop capture
|
label: Stop
|
||||||
name: b_stop
|
name: b_stop
|
||||||
callback: cb_stop
|
callback: cb_stop
|
||||||
argument: 0
|
argument: 0
|
||||||
|
|
@ -316,7 +346,7 @@ class: 20000
|
||||||
type: 0
|
type: 0
|
||||||
box: 0.000000 0.000000 0.000000 0.000000
|
box: 0.000000 0.000000 0.000000 0.000000
|
||||||
boxtype: 0
|
boxtype: 0
|
||||||
colors: 0 0
|
colors: 544171552 1331849829
|
||||||
alignment: 4
|
alignment: 4
|
||||||
style: 0
|
style: 0
|
||||||
size: 11.000000
|
size: 11.000000
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue