mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-07 07:14:56 +01:00
Support cut / copy / paste of MIDI automation.
git-svn-id: svn://localhost/ardour2/branches/3.0@7545 d708f5d6-7413-0410-9779-e7cbd77b26cf
This commit is contained in:
parent
e7a2b99f3d
commit
5e3ca4db5c
16 changed files with 169 additions and 67 deletions
|
|
@ -947,15 +947,25 @@ AutomationLine::remove_point (ControlPoint& cp)
|
|||
trackview.editor().session()->set_dirty ();
|
||||
}
|
||||
|
||||
/** Get selectable points within an area.
|
||||
* @param start Start position in session frames.
|
||||
* @param end End position in session frames.
|
||||
* @param botfrac Bottom of area, as a fraction of the line height.
|
||||
* @param topfrac Bottom of area, as a fraction of the line height.
|
||||
*/
|
||||
void
|
||||
AutomationLine::get_selectables (nframes_t start, nframes_t end,
|
||||
double botfrac, double topfrac, list<Selectable*>& results)
|
||||
AutomationLine::get_selectables (
|
||||
framepos_t start, framepos_t end, double botfrac, double topfrac, list<Selectable*>& results
|
||||
)
|
||||
{
|
||||
|
||||
double top;
|
||||
double bot;
|
||||
sframes_t nstart;
|
||||
sframes_t nend;
|
||||
|
||||
/* these two are in AutomationList model coordinates */
|
||||
double nstart;
|
||||
double nend;
|
||||
|
||||
bool collecting = false;
|
||||
|
||||
/* Curse X11 and its inverted coordinate system! */
|
||||
|
|
@ -963,21 +973,22 @@ AutomationLine::get_selectables (nframes_t start, nframes_t end,
|
|||
bot = (1.0 - topfrac) * _height;
|
||||
top = (1.0 - botfrac) * _height;
|
||||
|
||||
nstart = max_frames;
|
||||
nstart = DBL_MAX;
|
||||
nend = 0;
|
||||
|
||||
for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
|
||||
sframes_t const when = _time_converter.to ((*(*i)->model())->when);
|
||||
double const model_when = (*(*i)->model())->when;
|
||||
framepos_t const session_frames_when = _time_converter.to (model_when) + _time_converter.origin_b ();
|
||||
|
||||
if (when >= start && when <= end) {
|
||||
if (session_frames_when >= start && session_frames_when <= end) {
|
||||
|
||||
if ((*i)->get_y() >= bot && (*i)->get_y() <= top) {
|
||||
|
||||
(*i)->show();
|
||||
(*i)->set_visible(true);
|
||||
collecting = true;
|
||||
nstart = min (nstart, when);
|
||||
nend = max (nend, when);
|
||||
nstart = min (nstart, model_when);
|
||||
nend = max (nend, model_when);
|
||||
|
||||
} else {
|
||||
|
||||
|
|
@ -985,7 +996,7 @@ AutomationLine::get_selectables (nframes_t start, nframes_t end,
|
|||
|
||||
results.push_back (new AutomationSelectable (nstart, nend, botfrac, topfrac, &trackview));
|
||||
collecting = false;
|
||||
nstart = max_frames;
|
||||
nstart = DBL_MAX;
|
||||
nend = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1023,8 +1034,8 @@ AutomationLine::point_selection_to_control_points (PointSelection const & s)
|
|||
|
||||
for (vector<ControlPoint*>::iterator j = control_points.begin(); j != control_points.end(); ++j) {
|
||||
|
||||
double const rstart = trackview.editor().frame_to_unit (i->start);
|
||||
double const rend = trackview.editor().frame_to_unit (i->end);
|
||||
double const rstart = trackview.editor().frame_to_unit (_time_converter.to (i->start));
|
||||
double const rend = trackview.editor().frame_to_unit (_time_converter.to (i->end));
|
||||
|
||||
if ((*j)->get_x() >= rstart && (*j)->get_x() <= rend) {
|
||||
if ((*j)->get_y() >= bot && (*j)->get_y() <= top) {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class AutomationLine : public sigc::trackable, public PBD::StatefulDestructible
|
|||
|
||||
std::list<ControlPoint*> point_selection_to_control_points (PointSelection const &);
|
||||
void set_selected_points (PointSelection&);
|
||||
void get_selectables (nframes_t start, nframes_t end,
|
||||
void get_selectables (ARDOUR::framepos_t start, ARDOUR::framepos_t end,
|
||||
double botfrac, double topfrac,
|
||||
std::list<Selectable*>& results);
|
||||
void get_inverted_selectables (Selection&, std::list<Selectable*>& results);
|
||||
|
|
@ -136,6 +136,10 @@ class AutomationLine : public sigc::trackable, public PBD::StatefulDestructible
|
|||
|
||||
virtual MementoCommandBinder<ARDOUR::AutomationList>* memento_command_binder ();
|
||||
|
||||
const Evoral::TimeConverter<double, ARDOUR::sframes_t>& time_converter () const {
|
||||
return _time_converter;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
std::string _name;
|
||||
|
|
|
|||
|
|
@ -20,35 +20,37 @@
|
|||
#ifndef __ardour_gtk_automation_selectable_h__
|
||||
#define __ardour_gtk_automation_selectable_h__
|
||||
|
||||
#include "ardour/types.h"
|
||||
#include "selectable.h"
|
||||
|
||||
class TimeAxisView;
|
||||
|
||||
/** A selected automation point, expressed as a rectangle on a track (so that x coordinates
|
||||
* are frames and y coordinates are a fraction of track height). This representation falls
|
||||
* between the visible GUI control points and the back-end "actual" automation points,
|
||||
* some of which may not be visible; it is not trivial to convert from one of these to the other,
|
||||
* so the AutomationSelectable is a kind of "best and worst of both worlds".
|
||||
/** A selected automation point, expressed as a rectangle.
|
||||
* x coordinates start/end are in AutomationList model coordinates.
|
||||
* y coordinates are a expressed as a fraction of track height.
|
||||
* This representation falls between the visible GUI control points and
|
||||
* the back-end "actual" automation points, some of which may not be
|
||||
* visible; it is not trivial to convert from one of these to the
|
||||
* other, so the AutomationSelectable is a kind of "best and worst of
|
||||
* both worlds".
|
||||
*/
|
||||
struct AutomationSelectable : public Selectable
|
||||
{
|
||||
nframes_t start;
|
||||
nframes_t end;
|
||||
double low_fract;
|
||||
double high_fract;
|
||||
TimeAxisView* track; // ref would be better, but ARDOUR::SessionHandlePtr is non-assignable
|
||||
|
||||
AutomationSelectable (nframes_t s, nframes_t e, double l, double h, TimeAxisView* atv)
|
||||
: start (s), end (e), low_fract (l), high_fract (h), track (atv) {}
|
||||
|
||||
bool operator== (const AutomationSelectable& other) {
|
||||
return start == other.start &&
|
||||
end == other.end &&
|
||||
low_fract == other.low_fract &&
|
||||
high_fract == other.high_fract &&
|
||||
track == other.track;
|
||||
}
|
||||
double start;
|
||||
double end;
|
||||
double low_fract;
|
||||
double high_fract;
|
||||
TimeAxisView* track; // ref would be better, but ARDOUR::SessionHandlePtr is non-assignable
|
||||
|
||||
AutomationSelectable (double s, double e, double l, double h, TimeAxisView* atv)
|
||||
: start (s), end (e), low_fract (l), high_fract (h), track (atv) {}
|
||||
|
||||
bool operator== (const AutomationSelectable& other) {
|
||||
return start == other.start &&
|
||||
end == other.end &&
|
||||
low_fract == other.low_fract &&
|
||||
high_fract == other.high_fract &&
|
||||
track == other.track;
|
||||
}
|
||||
};
|
||||
|
||||
#endif /* __ardour_gtk_automation_selectable_h__ */
|
||||
|
|
|
|||
|
|
@ -274,13 +274,16 @@ AutomationStreamView::clear ()
|
|||
}
|
||||
}
|
||||
|
||||
/** @param start Start position in session frames.
|
||||
* @param end End position in session frames.
|
||||
*/
|
||||
void
|
||||
AutomationStreamView::get_selectables (nframes_t start, nframes_t end, double botfrac, double topfrac, list<Selectable*>& results)
|
||||
AutomationStreamView::get_selectables (framepos_t start, framepos_t end, double botfrac, double topfrac, list<Selectable*>& results)
|
||||
{
|
||||
for (list<RegionView*>::iterator i = region_views.begin(); i != region_views.end(); ++i) {
|
||||
AutomationRegionView* arv = dynamic_cast<AutomationRegionView*> (*i);
|
||||
assert (arv);
|
||||
arv->line()->get_selectables (start - (*i)->region()->position(), end - (*i)->region()->position(), botfrac, topfrac, results);
|
||||
arv->line()->get_selectables (start, end, botfrac, topfrac, results);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -307,3 +310,46 @@ AutomationStreamView::get_lines () const
|
|||
|
||||
return lines;
|
||||
}
|
||||
|
||||
struct RegionPositionSorter {
|
||||
bool operator() (RegionView* a, RegionView* b) {
|
||||
return a->region()->position() < b->region()->position();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @param pos Position, in session frames.
|
||||
* @return AutomationLine to paste to for that position, or 0 if there is none appropriate.
|
||||
*/
|
||||
boost::shared_ptr<AutomationLine>
|
||||
AutomationStreamView::paste_line (framepos_t pos)
|
||||
{
|
||||
/* XXX: not sure how best to pick this; for now, just use the last region which starts before pos */
|
||||
|
||||
if (region_views.empty()) {
|
||||
return boost::shared_ptr<AutomationLine> ();
|
||||
}
|
||||
|
||||
region_views.sort (RegionPositionSorter ());
|
||||
|
||||
list<RegionView*>::const_iterator prev = region_views.begin ();
|
||||
|
||||
for (list<RegionView*>::const_iterator i = region_views.begin(); i != region_views.end(); ++i) {
|
||||
if ((*i)->region()->position() > pos) {
|
||||
break;
|
||||
}
|
||||
prev = i;
|
||||
}
|
||||
|
||||
boost::shared_ptr<Region> r = (*prev)->region ();
|
||||
|
||||
/* If *prev doesn't cover pos, it's no good */
|
||||
if (r->position() > pos || ((r->position() + r->length()) < pos)) {
|
||||
return boost::shared_ptr<AutomationLine> ();
|
||||
}
|
||||
|
||||
AutomationRegionView* arv = dynamic_cast<AutomationRegionView*> (*prev);
|
||||
assert (arv);
|
||||
|
||||
return arv->line ();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,10 +61,11 @@ class AutomationStreamView : public StreamView
|
|||
|
||||
void clear ();
|
||||
|
||||
void get_selectables (nframes_t, nframes_t, double, double, std::list<Selectable*> &);
|
||||
void get_selectables (ARDOUR::framepos_t, ARDOUR::framepos_t, double, double, std::list<Selectable*> &);
|
||||
void set_selected_points (PointSelection &);
|
||||
|
||||
std::list<boost::shared_ptr<AutomationLine> > get_lines () const;
|
||||
boost::shared_ptr<AutomationLine> paste_line (ARDOUR::framepos_t);
|
||||
|
||||
private:
|
||||
void setup_rec_box ();
|
||||
|
|
|
|||
|
|
@ -634,21 +634,27 @@ AutomationTimeAxisView::cut_copy_clear_one (AutomationLine& line, Selection& sel
|
|||
|
||||
XMLNode &before = alist->get_state();
|
||||
|
||||
/* convert time selection to automation list model coordinates */
|
||||
const Evoral::TimeConverter<double, ARDOUR::sframes_t>& tc = line.time_converter ();
|
||||
double const start = tc.from (selection.time.front().start - tc.origin_b ());
|
||||
double const end = tc.from (selection.time.front().end - tc.origin_b ());
|
||||
|
||||
switch (op) {
|
||||
case Cut:
|
||||
if ((what_we_got = alist->cut (selection.time.front().start, selection.time.front().end)) != 0) {
|
||||
|
||||
if ((what_we_got = alist->cut (start, end)) != 0) {
|
||||
_editor.get_cut_buffer().add (what_we_got);
|
||||
_session->add_command(new MementoCommand<AutomationList>(*alist.get(), &before, &alist->get_state()));
|
||||
}
|
||||
break;
|
||||
case Copy:
|
||||
if ((what_we_got = alist->copy (selection.time.front().start, selection.time.front().end)) != 0) {
|
||||
if ((what_we_got = alist->copy (start, end)) != 0) {
|
||||
_editor.get_cut_buffer().add (what_we_got);
|
||||
}
|
||||
break;
|
||||
|
||||
case Clear:
|
||||
if ((what_we_got = alist->cut (selection.time.front().start, selection.time.front().end)) != 0) {
|
||||
if ((what_we_got = alist->cut (start, end)) != 0) {
|
||||
_session->add_command(new MementoCommand<AutomationList>(*alist.get(), &before, &alist->get_state()));
|
||||
}
|
||||
break;
|
||||
|
|
@ -740,8 +746,6 @@ AutomationTimeAxisView::cut_copy_clear_objects_one (AutomationLine& line, PointS
|
|||
|
||||
delete &before;
|
||||
|
||||
cout << "CCC objects " << what_we_got->size() << "\n";
|
||||
|
||||
if (what_we_got) {
|
||||
for (AutomationList::iterator x = what_we_got->begin(); x != what_we_got->end(); ++x) {
|
||||
double when = (*x)->when;
|
||||
|
|
@ -753,14 +757,32 @@ AutomationTimeAxisView::cut_copy_clear_objects_one (AutomationLine& line, PointS
|
|||
}
|
||||
}
|
||||
|
||||
/** Paste a selection.
|
||||
* @param pos Position to paste to (session frames).
|
||||
* @param times Number of times to paste.
|
||||
* @param selection Selection to paste.
|
||||
* @param nth Index of the AutomationList within the selection to paste from.
|
||||
*/
|
||||
bool
|
||||
AutomationTimeAxisView::paste (nframes_t pos, float times, Selection& selection, size_t nth)
|
||||
AutomationTimeAxisView::paste (framepos_t pos, float times, Selection& selection, size_t nth)
|
||||
{
|
||||
return paste_one (*_line, pos, times, selection, nth);
|
||||
boost::shared_ptr<AutomationLine> line;
|
||||
|
||||
if (_line) {
|
||||
line = _line;
|
||||
} else if (_view) {
|
||||
line = _view->paste_line (pos);
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return paste_one (*line, pos, times, selection, nth);
|
||||
}
|
||||
|
||||
bool
|
||||
AutomationTimeAxisView::paste_one (AutomationLine& line, nframes_t pos, float times, Selection& selection, size_t nth)
|
||||
AutomationTimeAxisView::paste_one (AutomationLine& line, framepos_t pos, float times, Selection& selection, size_t nth)
|
||||
{
|
||||
AutomationSelection::iterator p;
|
||||
boost::shared_ptr<AutomationList> alist(line.the_list());
|
||||
|
|
@ -786,8 +808,10 @@ AutomationTimeAxisView::paste_one (AutomationLine& line, nframes_t pos, float ti
|
|||
(*x)->value = val;
|
||||
}
|
||||
|
||||
double const model_pos = line.time_converter().from (pos - line.time_converter().origin_b ());
|
||||
|
||||
XMLNode &before = alist->get_state();
|
||||
alist->paste (copy, pos, times);
|
||||
alist->paste (copy, model_pos, times);
|
||||
_session->add_command (new MementoCommand<AutomationList>(*alist.get(), &before, &alist->get_state()));
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class AutomationTimeAxisView : public TimeAxisView {
|
|||
|
||||
void cut_copy_clear (Selection&, Editing::CutCopyOp);
|
||||
void cut_copy_clear_objects (PointSelection&, Editing::CutCopyOp);
|
||||
bool paste (nframes_t, float times, Selection&, size_t nth);
|
||||
bool paste (ARDOUR::framepos_t, float times, Selection&, size_t nth);
|
||||
void reset_objects (PointSelection&);
|
||||
|
||||
int set_state (const XMLNode&, int version);
|
||||
|
|
@ -148,7 +148,7 @@ class AutomationTimeAxisView : public TimeAxisView {
|
|||
|
||||
void cut_copy_clear_one (AutomationLine&, Selection&, Editing::CutCopyOp);
|
||||
void cut_copy_clear_objects_one (AutomationLine&, PointSelection&, Editing::CutCopyOp);
|
||||
bool paste_one (AutomationLine&, nframes_t, float times, Selection&, size_t nth);
|
||||
bool paste_one (AutomationLine&, ARDOUR::framepos_t, float times, Selection&, size_t nth);
|
||||
void reset_objects_one (AutomationLine&, PointSelection&);
|
||||
|
||||
void set_automation_state (ARDOUR::AutoState);
|
||||
|
|
|
|||
|
|
@ -977,11 +977,13 @@ Editor::invert_selection ()
|
|||
selection->set (touched);
|
||||
}
|
||||
|
||||
/** @param top Top (lower) y limit in trackview coordinates.
|
||||
/** @param start Start time in session frames.
|
||||
* @param end End time in session frames.
|
||||
* @param top Top (lower) y limit in trackview coordinates.
|
||||
* @param bottom Bottom (higher) y limit in trackview coordinates.
|
||||
*/
|
||||
bool
|
||||
Editor::select_all_within (nframes64_t start, nframes64_t end, double top, double bot, const TrackViewList& tracklist, Selection::Operation op)
|
||||
Editor::select_all_within (framepos_t start, framepos_t end, double top, double bot, const TrackViewList& tracklist, Selection::Operation op)
|
||||
{
|
||||
list<Selectable*> found;
|
||||
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ RegionView::region_resized (const PropertyChange& what_changed)
|
|||
|
||||
if (what_changed.contains (ARDOUR::Properties::position)) {
|
||||
set_position (_region->position(), 0);
|
||||
_time_converter.set_origin(_region->position());
|
||||
_time_converter.set_origin_b (_region->position());
|
||||
}
|
||||
|
||||
PropertyChange s_and_l;
|
||||
|
|
|
|||
|
|
@ -1363,7 +1363,7 @@ RouteTimeAxisView::cut_copy_clear (Selection& selection, CutCopyOp op)
|
|||
}
|
||||
|
||||
bool
|
||||
RouteTimeAxisView::paste (nframes_t pos, float times, Selection& selection, size_t nth)
|
||||
RouteTimeAxisView::paste (framepos_t pos, float times, Selection& selection, size_t nth)
|
||||
{
|
||||
if (!is_track()) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ public:
|
|||
|
||||
/* Editing operations */
|
||||
void cut_copy_clear (Selection&, Editing::CutCopyOp);
|
||||
bool paste (nframes_t, float times, Selection&, size_t nth);
|
||||
bool paste (ARDOUR::framepos_t, float times, Selection&, size_t nth);
|
||||
|
||||
TimeAxisView::Children get_child_list();
|
||||
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ class TimeAxisView : public virtual AxisView, public PBD::Stateful
|
|||
/* editing operations */
|
||||
|
||||
virtual void cut_copy_clear (Selection&, Editing::CutCopyOp) {}
|
||||
virtual bool paste (nframes_t, float /*times*/, Selection&, size_t /*nth*/) { return false; }
|
||||
virtual bool paste (ARDOUR::framepos_t, float /*times*/, Selection&, size_t /*nth*/) { return false; }
|
||||
|
||||
virtual void set_selected_regionviews (RegionSelection&) {}
|
||||
virtual void set_selected_points (PointSelection&) {}
|
||||
|
|
|
|||
|
|
@ -32,19 +32,15 @@ class TempoMap;
|
|||
class BeatsFramesConverter : public Evoral::TimeConverter<double,sframes_t> {
|
||||
public:
|
||||
BeatsFramesConverter(const TempoMap& tempo_map, sframes_t origin)
|
||||
: _tempo_map(tempo_map)
|
||||
, _origin(origin)
|
||||
: Evoral::TimeConverter<double, sframes_t> (origin)
|
||||
, _tempo_map(tempo_map)
|
||||
{}
|
||||
|
||||
sframes_t to(double beats) const;
|
||||
double from(sframes_t frames) const;
|
||||
|
||||
sframes_t origin() const { return _origin; }
|
||||
void set_origin(sframes_t origin) { _origin = origin; }
|
||||
|
||||
private:
|
||||
const TempoMap& _tempo_map;
|
||||
sframes_t _origin;
|
||||
};
|
||||
|
||||
} /* namespace ARDOUR */
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ sframes_t
|
|||
BeatsFramesConverter::to(double beats) const
|
||||
{
|
||||
// FIXME: assumes tempo never changes after origin
|
||||
const Tempo& tempo = _tempo_map.tempo_at(_origin);
|
||||
const Tempo& tempo = _tempo_map.tempo_at (_origin_b);
|
||||
const double frames_per_beat = tempo.frames_per_beat(
|
||||
_tempo_map.frame_rate(),
|
||||
_tempo_map.meter_at(_origin));
|
||||
_tempo_map.meter_at (_origin_b));
|
||||
|
||||
return lrint(beats * frames_per_beat);
|
||||
}
|
||||
|
|
@ -40,10 +40,10 @@ double
|
|||
BeatsFramesConverter::from(sframes_t frames) const
|
||||
{
|
||||
// FIXME: assumes tempo never changes after origin
|
||||
const Tempo& tempo = _tempo_map.tempo_at(_origin);
|
||||
const Tempo& tempo = _tempo_map.tempo_at (_origin_b);
|
||||
const double frames_per_beat = tempo.frames_per_beat(
|
||||
_tempo_map.frame_rate(),
|
||||
_tempo_map.meter_at(_origin));
|
||||
_tempo_map.meter_at (_origin_b));
|
||||
|
||||
return frames / frames_per_beat;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ namespace Evoral {
|
|||
template<typename A, typename B>
|
||||
class TimeConverter {
|
||||
public:
|
||||
TimeConverter (B ob = 0) : _origin_b (ob) {}
|
||||
virtual ~TimeConverter() {}
|
||||
|
||||
/** Convert A time to B time (A to B) */
|
||||
|
|
@ -36,6 +37,17 @@ public:
|
|||
|
||||
/** Convert B time to A time (A from B) */
|
||||
virtual A from(B b) const = 0;
|
||||
|
||||
B origin_b () const {
|
||||
return _origin_b;
|
||||
}
|
||||
|
||||
void set_origin_b (B o) {
|
||||
_origin_b = o;
|
||||
}
|
||||
|
||||
protected:
|
||||
B _origin_b;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1145,7 +1145,10 @@ ControlList::cut (iterator start, iterator end)
|
|||
return nal;
|
||||
}
|
||||
|
||||
/** @param op 0 = cut, 1 = copy, 2 = clear */
|
||||
/** @param start Start position in model coordinates.
|
||||
* @param end End position in model coordinates.
|
||||
* @param op 0 = cut, 1 = copy, 2 = clear.
|
||||
*/
|
||||
boost::shared_ptr<ControlList>
|
||||
ControlList::cut_copy_clear (double start, double end, int op)
|
||||
{
|
||||
|
|
@ -1247,9 +1250,10 @@ ControlList::copy (double start, double end)
|
|||
void
|
||||
ControlList::clear (double start, double end)
|
||||
{
|
||||
(void) cut_copy_clear (start, end, 2);
|
||||
cut_copy_clear (start, end, 2);
|
||||
}
|
||||
|
||||
/** @param pos Position in model coordinates */
|
||||
bool
|
||||
ControlList::paste (ControlList& alist, double pos, float /*times*/)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue