mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
show the popup at the moment of hover
This commit is contained in:
parent
74bdd09a16
commit
5cf146ee86
2 changed files with 147 additions and 134 deletions
|
|
@ -843,10 +843,18 @@ ActionVariantWidget[state="hover"], #OptionalActionOption[state="hover"] {
|
|||
}
|
||||
|
||||
LauncherSettingsButton {
|
||||
icon-size: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #f4f5f5;
|
||||
padding: 1px 3px 1px 3px;
|
||||
}
|
||||
|
||||
LauncherSettingsButton:hover {
|
||||
LauncherSettingsButton[inMenu="1"] {
|
||||
border: none;
|
||||
padding: 3px 5px 3px 5px;
|
||||
}
|
||||
|
||||
LauncherSettingsButton:hover, LauncherSettingsButton[inMenu="1"]:hover {
|
||||
border: 1px solid #f4f5f5;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from ayon_core.tools.flickcharm import FlickCharm
|
|||
from ayon_core.tools.utils import (
|
||||
get_qt_icon,
|
||||
SquareButton,
|
||||
ClickableLabel,
|
||||
ClickableFrame,
|
||||
PixmapLabel,
|
||||
)
|
||||
from ayon_core.tools.attribute_defs import AttributeDefinitionsDialog
|
||||
|
|
@ -30,11 +30,12 @@ ANIMATION_LEN = 7
|
|||
ACTION_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
ACTION_TYPE_ROLE = QtCore.Qt.UserRole + 2
|
||||
ACTION_IS_GROUP_ROLE = QtCore.Qt.UserRole + 3
|
||||
ACTION_SORT_ROLE = QtCore.Qt.UserRole + 4
|
||||
ACTION_ADDON_NAME_ROLE = QtCore.Qt.UserRole + 5
|
||||
ACTION_ADDON_VERSION_ROLE = QtCore.Qt.UserRole + 6
|
||||
ANIMATION_START_ROLE = QtCore.Qt.UserRole + 7
|
||||
ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 8
|
||||
ACTION_HAS_CONFIGS_ROLE = QtCore.Qt.UserRole + 4
|
||||
ACTION_SORT_ROLE = QtCore.Qt.UserRole + 5
|
||||
ACTION_ADDON_NAME_ROLE = QtCore.Qt.UserRole + 6
|
||||
ACTION_ADDON_VERSION_ROLE = QtCore.Qt.UserRole + 7
|
||||
ANIMATION_START_ROLE = QtCore.Qt.UserRole + 8
|
||||
ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 9
|
||||
|
||||
|
||||
def _variant_label_sort_getter(action_item):
|
||||
|
|
@ -69,22 +70,51 @@ class LauncherSettingsButton(SquareButton):
|
|||
})
|
||||
return cls._settings_icon
|
||||
|
||||
def set_is_in_menu(self, in_menu):
|
||||
value = "1" if in_menu else ""
|
||||
self.setProperty("inMenu", value)
|
||||
self.style().polish(self)
|
||||
|
||||
class ActionVariantWidget(QtWidgets.QFrame):
|
||||
|
||||
class ActionOverlayWidget(QtWidgets.QFrame):
|
||||
config_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, item_id, parent):
|
||||
super().__init__(parent)
|
||||
self._item_id = item_id
|
||||
|
||||
settings_btn = LauncherSettingsButton(self)
|
||||
|
||||
main_layout = QtWidgets.QGridLayout(self)
|
||||
main_layout.setContentsMargins(5, 5, 0, 0)
|
||||
main_layout.addWidget(settings_btn, 0, 0)
|
||||
main_layout.setColumnStretch(1, 1)
|
||||
main_layout.setRowStretch(1, 1)
|
||||
|
||||
settings_btn.clicked.connect(self._on_settings_click)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
def _on_settings_click(self):
|
||||
self.config_requested.emit(self._item_id)
|
||||
|
||||
|
||||
class ActionVariantWidget(ClickableFrame):
|
||||
action_triggered = QtCore.Signal(str)
|
||||
settings_requested = QtCore.Signal(str)
|
||||
config_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, item_id, icon, label, has_settings, parent):
|
||||
def __init__(self, item_id, icon, label, has_configs, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
icon_widget = None
|
||||
if icon:
|
||||
icon_widget = PixmapLabel(icon.pixmap(512, 512), self)
|
||||
|
||||
label_widget = ClickableLabel(label, self)
|
||||
label_widget = QtWidgets.QLabel(label, self)
|
||||
settings_btn = None
|
||||
if has_settings:
|
||||
if has_configs:
|
||||
settings_btn = LauncherSettingsButton(self)
|
||||
settings_btn.set_is_in_menu(True)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(6, 4, 4, 4)
|
||||
|
|
@ -99,7 +129,7 @@ class ActionVariantWidget(QtWidgets.QFrame):
|
|||
layout.addWidget(settings_btn, 0)
|
||||
|
||||
settings_btn.clicked.connect(self._on_settings_clicked)
|
||||
label_widget.clicked.connect(self._on_trigger)
|
||||
self.clicked.connect(self._on_trigger)
|
||||
|
||||
self._item_id = item_id
|
||||
self._icon_widget = icon_widget
|
||||
|
|
@ -125,7 +155,7 @@ class ActionVariantWidget(QtWidgets.QFrame):
|
|||
self.action_triggered.emit(self._item_id)
|
||||
|
||||
def _on_settings_clicked(self):
|
||||
self.settings_requested.emit(self._item_id)
|
||||
self.config_requested.emit(self._item_id)
|
||||
|
||||
def _set_hover_properties(self, hovered):
|
||||
state = "hover" if hovered else ""
|
||||
|
|
@ -134,26 +164,6 @@ class ActionVariantWidget(QtWidgets.QFrame):
|
|||
self.style().polish(self)
|
||||
|
||||
|
||||
class ActionVariantAction(QtWidgets.QWidgetAction):
|
||||
"""Menu action with settings button."""
|
||||
settings_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, item_id, label, has_settings, parent):
|
||||
super().__init__(parent)
|
||||
self._item_id = item_id
|
||||
self._label = label
|
||||
self._has_settings = has_settings
|
||||
self._widget = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
widget = ActionVariantWidget(
|
||||
self._item_id, self._label, self._has_settings, parent
|
||||
)
|
||||
widget.settings_requested.connect(self.settings_requested)
|
||||
self._widget = widget
|
||||
return widget
|
||||
|
||||
|
||||
class ActionsQtModel(QtGui.QStandardItemModel):
|
||||
"""Qt model for actions.
|
||||
|
||||
|
|
@ -214,7 +224,6 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
for item in items:
|
||||
if item.identifier == action_id:
|
||||
return self._items_by_id[group_id]
|
||||
|
||||
return None
|
||||
|
||||
def get_action_item_by_id(self, action_id):
|
||||
|
|
@ -276,9 +285,11 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
icon = get_qt_icon(transparent_icon.copy())
|
||||
|
||||
if is_group:
|
||||
has_configs = False
|
||||
label = action_item.label
|
||||
else:
|
||||
label = action_item.full_label
|
||||
has_configs = bool(action_item.config_fields)
|
||||
|
||||
item = self._items_by_id.get(action_item.identifier)
|
||||
if item is None:
|
||||
|
|
@ -290,6 +301,7 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
item.setData(label, QtCore.Qt.DisplayRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(is_group, ACTION_IS_GROUP_ROLE)
|
||||
item.setData(has_configs, ACTION_HAS_CONFIGS_ROLE)
|
||||
item.setData(action_item.action_type, ACTION_TYPE_ROLE)
|
||||
item.setData(action_item.addon_name, ACTION_ADDON_NAME_ROLE)
|
||||
item.setData(action_item.addon_version, ACTION_ADDON_VERSION_ROLE)
|
||||
|
|
@ -335,11 +347,14 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
self.refresh()
|
||||
|
||||
|
||||
class ActionMenuToolTip(QtWidgets.QFrame):
|
||||
class ActionMenuPopup(QtWidgets.QFrame):
|
||||
action_triggered = QtCore.Signal(str)
|
||||
config_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowFlags(QtCore.Qt.ToolTip)
|
||||
self.setWindowFlags(QtCore.Qt.Popup)
|
||||
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating, True)
|
||||
self.setAutoFillBackground(True)
|
||||
self.setBackgroundRole(QtGui.QPalette.Base)
|
||||
|
|
@ -354,38 +369,29 @@ class ActionMenuToolTip(QtWidgets.QFrame):
|
|||
close_timer.setSingleShot(True)
|
||||
close_timer.setInterval(100)
|
||||
|
||||
update_state_timer = QtCore.QTimer()
|
||||
update_state_timer.setInterval(500)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
close_timer.timeout.connect(self.close)
|
||||
show_timer.timeout.connect(self._on_show_timer)
|
||||
update_state_timer.timeout.connect(self._on_update_state)
|
||||
|
||||
self._main_layout = main_layout
|
||||
self._show_timer = show_timer
|
||||
self._close_timer = close_timer
|
||||
self._update_state_timer = update_state_timer
|
||||
|
||||
self._showed = False
|
||||
self._mouse_entered = False
|
||||
self._view_hovered = False
|
||||
self._current_id = None
|
||||
self._view = None
|
||||
self._last_pos = QtCore.QPoint(0, 0)
|
||||
self._widgets_by_id = {}
|
||||
|
||||
def showEvent(self, event):
|
||||
self._showed = True
|
||||
self._update_state_timer.start()
|
||||
super().showEvent(event)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._showed = False
|
||||
self._update_state_timer.stop()
|
||||
self._mouse_entered = False
|
||||
super().closeEvent(event)
|
||||
|
||||
|
|
@ -397,27 +403,15 @@ class ActionMenuToolTip(QtWidgets.QFrame):
|
|||
def leaveEvent(self, event):
|
||||
self._mouse_entered = False
|
||||
super().leaveEvent(event)
|
||||
if not self._view_hovered:
|
||||
self._close_timer.start()
|
||||
self._close_timer.start()
|
||||
|
||||
def mouse_entered_view(self):
|
||||
self._view_hovered = True
|
||||
|
||||
def mouse_left_view(self):
|
||||
self._view_hovered = False
|
||||
if not self._mouse_entered:
|
||||
self._close_timer.start()
|
||||
|
||||
def show_on_event(self, action_id, action_items, view, event):
|
||||
def show_items(self, action_id, action_items, pos):
|
||||
self._close_timer.stop()
|
||||
|
||||
self._view_hovered = True
|
||||
|
||||
is_current = action_id == self._current_id
|
||||
if not is_current:
|
||||
self._current_id = action_id
|
||||
self._view = view
|
||||
self._update_items(view, action_items)
|
||||
self._update_items(action_items)
|
||||
|
||||
# Nothing to show
|
||||
if not self._widgets_by_id:
|
||||
|
|
@ -431,7 +425,6 @@ class ActionMenuToolTip(QtWidgets.QFrame):
|
|||
update_position = True
|
||||
self.show()
|
||||
|
||||
self._last_pos = QtCore.QPoint(event.globalPos())
|
||||
if not update_position:
|
||||
# Only resize if is current
|
||||
self.resize(self.sizeHint())
|
||||
|
|
@ -441,7 +434,12 @@ class ActionMenuToolTip(QtWidgets.QFrame):
|
|||
# are recalculated
|
||||
app = QtWidgets.QApplication.instance()
|
||||
app.processEvents()
|
||||
self._on_update_state()
|
||||
size = self.sizeHint()
|
||||
offset = 4
|
||||
self.setGeometry(
|
||||
pos.x() + offset, pos.y() + offset,
|
||||
size.width(), size.height()
|
||||
)
|
||||
|
||||
self.raise_()
|
||||
self._show_timer.start()
|
||||
|
|
@ -450,18 +448,7 @@ class ActionMenuToolTip(QtWidgets.QFrame):
|
|||
size = self.sizeHint()
|
||||
self.resize(size)
|
||||
|
||||
def _on_update_state(self):
|
||||
if not self._view_hovered:
|
||||
return
|
||||
size = self.sizeHint()
|
||||
pos = self._last_pos
|
||||
offset = 4
|
||||
self.setGeometry(
|
||||
pos.x() + offset, pos.y() + offset,
|
||||
size.width(), size.height()
|
||||
)
|
||||
|
||||
def _update_items(self, view, action_items):
|
||||
def _update_items(self, action_items):
|
||||
"""Update items in the tooltip."""
|
||||
# This method can be used to update the content of the tooltip
|
||||
# with new icon, text and settings button visibility.
|
||||
|
|
@ -470,16 +457,16 @@ class ActionMenuToolTip(QtWidgets.QFrame):
|
|||
new_ids = set()
|
||||
widgets = []
|
||||
|
||||
any_has_settings = False
|
||||
any_has_configs = False
|
||||
prepared_items = []
|
||||
for idx, action_item in enumerate(action_items):
|
||||
has_settings = bool(action_item.config_fields)
|
||||
if has_settings:
|
||||
any_has_settings = True
|
||||
prepared_items.append((idx, action_item, has_settings))
|
||||
has_configs = bool(action_item.config_fields)
|
||||
if has_configs:
|
||||
any_has_configs = True
|
||||
prepared_items.append((idx, action_item, has_configs))
|
||||
|
||||
if any_has_settings or len(action_items) > 1:
|
||||
for idx, action_item, has_settings in prepared_items:
|
||||
if any_has_configs or len(action_items) > 1:
|
||||
for idx, action_item, has_configs in prepared_items:
|
||||
widget = self._widgets_by_id.get(action_item.identifier)
|
||||
icon = get_qt_icon(action_item.icon)
|
||||
label = action_item.full_label
|
||||
|
|
@ -488,12 +475,12 @@ class ActionMenuToolTip(QtWidgets.QFrame):
|
|||
action_item.identifier,
|
||||
icon,
|
||||
label,
|
||||
has_settings,
|
||||
has_configs,
|
||||
self
|
||||
)
|
||||
widget.action_triggered.connect(self._on_trigger)
|
||||
widget.settings_requested.connect(
|
||||
self._on_settings_trigger
|
||||
widget.config_requested.connect(
|
||||
self._on_configs_trigger
|
||||
)
|
||||
new_ids.add(action_item.identifier)
|
||||
self._widgets_by_id[action_item.identifier] = widget
|
||||
|
|
@ -511,13 +498,12 @@ class ActionMenuToolTip(QtWidgets.QFrame):
|
|||
self._main_layout.insertWidget(idx, widget, 0)
|
||||
|
||||
def _on_trigger(self, action_id):
|
||||
self.action_triggered.emit(action_id)
|
||||
self.close()
|
||||
self._view.action_triggered.emit(action_id)
|
||||
|
||||
def _on_settings_trigger(self, action_id):
|
||||
"""Handle settings button click."""
|
||||
def _on_configs_trigger(self, action_id):
|
||||
self.config_requested.emit(action_id)
|
||||
self.close()
|
||||
self._view.settings_requested.emit(action_id)
|
||||
|
||||
|
||||
class ActionDelegate(QtWidgets.QStyledItemDelegate):
|
||||
|
|
@ -527,41 +513,12 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
super().__init__(*args, **kwargs)
|
||||
self._anim_start_color = QtGui.QColor(178, 255, 246)
|
||||
self._anim_end_color = QtGui.QColor(5, 44, 50)
|
||||
self._tooltip_widget = None
|
||||
|
||||
def helpEvent(self, event, view, option, index):
|
||||
if not index.isValid():
|
||||
if self._tooltip_widget is not None:
|
||||
self._tooltip_widget.close()
|
||||
return False
|
||||
def sizeHint(self, option, index):
|
||||
return option.widget.gridSize()
|
||||
|
||||
action_id = index.data(ACTION_ID_ROLE)
|
||||
model = index.model()
|
||||
source_model = model.sourceModel()
|
||||
if index.data(ACTION_IS_GROUP_ROLE):
|
||||
action_items = source_model.get_group_items(action_id)
|
||||
else:
|
||||
action_items = [source_model.get_action_item_by_id(action_id)]
|
||||
if self._tooltip_widget is None:
|
||||
self._tooltip_widget = ActionMenuToolTip(view)
|
||||
|
||||
self._tooltip_widget.show_on_event(
|
||||
action_id, action_items, view, event
|
||||
)
|
||||
event.setAccepted(True)
|
||||
return True
|
||||
|
||||
def close_tooltip(self):
|
||||
if self._tooltip_widget is not None:
|
||||
self._tooltip_widget.close()
|
||||
|
||||
def mouse_entered_view(self):
|
||||
if self._tooltip_widget is not None:
|
||||
self._tooltip_widget.mouse_entered_view()
|
||||
|
||||
def mouse_left_view(self):
|
||||
if self._tooltip_widget is not None:
|
||||
self._tooltip_widget.mouse_left_view()
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
editor.setGeometry(option.rect)
|
||||
|
||||
def _draw_animation(self, painter, option, index):
|
||||
grid_size = option.widget.gridSize()
|
||||
|
|
@ -683,7 +640,7 @@ class ActionsProxyModel(QtCore.QSortFilterProxyModel):
|
|||
|
||||
class ActionsView(QtWidgets.QListView):
|
||||
action_triggered = QtCore.Signal(str)
|
||||
settings_requested = QtCore.Signal(str)
|
||||
config_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
|
@ -699,7 +656,7 @@ class ActionsView(QtWidgets.QListView):
|
|||
self.setIconSize(QtCore.QSize(30, 30))
|
||||
self.setSpacing(0)
|
||||
self.setWordWrap(True)
|
||||
self.setToolTipDuration(150)
|
||||
self.setMouseTracking(True)
|
||||
|
||||
delegate = ActionDelegate(self)
|
||||
self.setItemDelegate(delegate)
|
||||
|
|
@ -708,16 +665,35 @@ class ActionsView(QtWidgets.QListView):
|
|||
flick = FlickCharm(parent=self)
|
||||
flick.activateOn(self)
|
||||
|
||||
popup_widget = ActionMenuPopup(self)
|
||||
|
||||
popup_widget.action_triggered.connect(self.action_triggered)
|
||||
popup_widget.config_requested.connect(self.config_requested)
|
||||
|
||||
self._flick = flick
|
||||
self._delegate = delegate
|
||||
self._popup_widget = popup_widget
|
||||
|
||||
def enterEvent(self, event):
|
||||
super().enterEvent(event)
|
||||
self._delegate.mouse_entered_view()
|
||||
def mouseMoveEvent(self, event):
|
||||
"""Handle mouse move event."""
|
||||
super().mouseMoveEvent(event)
|
||||
# Update hover state for the item under mouse
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
def leaveEvent(self, event):
|
||||
super().leaveEvent(event)
|
||||
self._delegate.mouse_left_view()
|
||||
if index.data(ACTION_IS_GROUP_ROLE):
|
||||
self._show_group_popup(index)
|
||||
|
||||
def _show_group_popup(self, index):
|
||||
action_id = index.data(ACTION_ID_ROLE)
|
||||
source_model = self.model().sourceModel()
|
||||
action_items = source_model.get_group_items(action_id)
|
||||
rect = self.rectForIndex(index)
|
||||
pos = self.mapToGlobal(rect.topLeft())
|
||||
self._popup_widget.show_items(
|
||||
action_id, action_items, pos
|
||||
)
|
||||
|
||||
|
||||
class ActionsWidget(QtWidgets.QWidget):
|
||||
|
|
@ -744,7 +720,7 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
|
||||
view.clicked.connect(self._on_clicked)
|
||||
view.action_triggered.connect(self._trigger_action)
|
||||
view.settings_requested.connect(self._show_config_dialog)
|
||||
view.config_requested.connect(self._on_config_request)
|
||||
model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
self._animated_items = set()
|
||||
|
|
@ -754,7 +730,7 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
self._model = model
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
self._config_widget = None
|
||||
self._overlay_widgets = []
|
||||
|
||||
self._set_row_height(1)
|
||||
|
||||
|
|
@ -808,6 +784,31 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
# Force repaint all items
|
||||
viewport = self._view.viewport()
|
||||
viewport.update()
|
||||
self._add_overlay_widgets()
|
||||
|
||||
def _add_overlay_widgets(self):
|
||||
overlay_widgets = []
|
||||
viewport = self._view.viewport()
|
||||
for row in range(self._proxy_model.rowCount()):
|
||||
index = self._proxy_model.index(row, 0)
|
||||
has_configs = index.data(ACTION_HAS_CONFIGS_ROLE)
|
||||
widget = None
|
||||
if has_configs:
|
||||
item_id = index.data(ACTION_ID_ROLE)
|
||||
widget = ActionOverlayWidget(item_id, viewport)
|
||||
widget.config_requested.connect(
|
||||
self._on_config_request
|
||||
)
|
||||
overlay_widgets.append(widget)
|
||||
self._view.setIndexWidget(index, widget)
|
||||
|
||||
while self._overlay_widgets:
|
||||
widget = self._overlay_widgets.pop(0)
|
||||
widget.setVisible(False)
|
||||
widget.setParent(None)
|
||||
widget.deleteLater()
|
||||
|
||||
self._overlay_widgets = overlay_widgets
|
||||
|
||||
def _on_animation(self):
|
||||
time_now = time.time()
|
||||
|
|
@ -842,8 +843,9 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
if not index or not index.isValid():
|
||||
return
|
||||
|
||||
_is_group = index.data(ACTION_IS_GROUP_ROLE)
|
||||
# TODO define and store what is default action for a group
|
||||
is_group = index.data(ACTION_IS_GROUP_ROLE)
|
||||
if is_group:
|
||||
return
|
||||
action_id = index.data(ACTION_ID_ROLE)
|
||||
self._trigger_action(action_id, index)
|
||||
|
||||
|
|
@ -879,6 +881,9 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
if index is not None:
|
||||
self._start_animation(index)
|
||||
|
||||
def _on_config_request(self, action_id):
|
||||
self._show_config_dialog(action_id)
|
||||
|
||||
def _show_config_dialog(self, action_id):
|
||||
action_item = self._model.get_action_item_by_id(action_id)
|
||||
config_fields = self._model.get_action_config_fields(action_id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue