Nils
4 years ago
7 changed files with 13 additions and 753 deletions
@ -1,332 +0,0 @@ |
|||||
#! /usr/bin/env python3 |
|
||||
# -*- coding: utf-8 -*- |
|
||||
""" |
|
||||
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) |
|
||||
|
|
||||
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). |
|
||||
|
|
||||
This application is free software: you can redistribute it and/or modify |
|
||||
it under the terms of the GNU General Public License as published by |
|
||||
the Free Software Foundation, either version 3 of the License, or |
|
||||
(at your option) any later version. |
|
||||
|
|
||||
This program is distributed in the hope that it will be useful, |
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
GNU General Public License for more details. |
|
||||
|
|
||||
You should have received a copy of the GNU General Public License |
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
||||
""" |
|
||||
|
|
||||
import logging; logger = logging.getLogger(__name__); logger.info("import") |
|
||||
|
|
||||
#Standard Library |
|
||||
|
|
||||
#Third Party |
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets |
|
||||
|
|
||||
#Engine |
|
||||
import engine.api as api |
|
||||
from engine.config import METADATA #includes METADATA only. No other environmental setup is executed. |
|
||||
|
|
||||
#QtGui |
|
||||
from .descriptiontextwidget import DescriptionController |
|
||||
from .resources import * |
|
||||
|
|
||||
|
|
||||
def nothing(): |
|
||||
pass |
|
||||
|
|
||||
class StarterClientItem(QtWidgets.QListWidgetItem): |
|
||||
""".desktop-like entry: |
|
||||
{'type': 'Application', |
|
||||
'name': 'Vico', |
|
||||
'genericname': 'Sequencer', |
|
||||
'comment':'Minimalistic midi sequencer with piano roll for JACK and NSM', |
|
||||
'exec': 'vico', |
|
||||
'icon': 'vico', |
|
||||
'terminal': 'false', |
|
||||
'startupnotify': 'false', |
|
||||
'categories': 'AudioVideo;Audio;X-Recorders;X-Multitrack;X-Jack;', |
|
||||
'x-nsm-capable': 'true', 'version': '1.0.1', |
|
||||
'agordejoFullPath': '/usr/bin/vico', |
|
||||
'agordejoExec': 'vico', |
|
||||
'whitelist': True} |
|
||||
|
|
||||
This is the icon that is starter and status-indicator at once. |
|
||||
QuickSession has only one icon per agordejoExec. |
|
||||
|
|
||||
If at least one program is running as nsmClient in the session we switch ourselves on and |
|
||||
save the status as self.nsmClientDict |
|
||||
|
|
||||
We do not react to name overrides by nsm-data, nor do we react to labels or name changes |
|
||||
through reportedNames. |
|
||||
""" |
|
||||
allItems = {} #agordejoExec:StarterClientItem |
|
||||
|
|
||||
def __init__(self, parentController, desktopEntry:dict): |
|
||||
self.parentController = parentController |
|
||||
self.desktopEntry = desktopEntry |
|
||||
self.agordejoExec = desktopEntry["agordejoExec"] |
|
||||
super().__init__(desktopEntry["name"], type=1000) #type 0 is default qt type. 1000 is subclassed user type) |
|
||||
|
|
||||
self.nsmClientDict = None #aka nsmStatusDict if this exists it means at least one instance of this application is running in the session |
|
||||
|
|
||||
if "comment" in desktopEntry: |
|
||||
self.setToolTip(desktopEntry["comment"]) |
|
||||
else: |
|
||||
self.setToolTip(desktopEntry["name"]) |
|
||||
programIcons = self.parentController.mainWindow.programIcons |
|
||||
assert programIcons |
|
||||
assert "agordejoExec" in desktopEntry, desktopEntry |
|
||||
if desktopEntry["agordejoExec"] in programIcons: |
|
||||
icon = programIcons[desktopEntry["agordejoExec"]] |
|
||||
self.setIcon(icon) |
|
||||
self.updateStatus(None) #removed/off |
|
||||
|
|
||||
|
|
||||
def updateStatus(self, clientDict:dict): |
|
||||
""" |
|
||||
api callback |
|
||||
|
|
||||
clientDict = { |
|
||||
"clientId":clientId, #for convenience, included internally as well |
|
||||
"dumbClient":True, #Bool. Real nsm or just any old program? status "Ready" switches this. |
|
||||
"reportedName":None, #str |
|
||||
"executable":None, #For dumb clients this is the same as reportedName. |
|
||||
"label":None, #str |
|
||||
"lastStatus":None, #str |
|
||||
"statusHistory":[], #list |
|
||||
"hasOptionalGUI": False, #bool |
|
||||
"visible": None, # bool |
|
||||
"dirty": None, # bool |
|
||||
} |
|
||||
""" |
|
||||
self.nsmClientDict = clientDict #for comparison with later status changes. Especially for stopped clients. |
|
||||
if clientDict is None: |
|
||||
self.removed() |
|
||||
else: |
|
||||
getattr(self, clientDict["lastStatus"], nothing)() |
|
||||
|
|
||||
def _setIconOverlay(self, status:str): |
|
||||
options = { |
|
||||
"removed": ":alert.svg", |
|
||||
"stopped": ":power.svg", |
|
||||
"hidden": ":hidden.svg", |
|
||||
"ready": ":running.svg", |
|
||||
} |
|
||||
|
|
||||
if status in options: |
|
||||
overlayPixmap = QtGui.QIcon(options[status]).pixmap(QtCore.QSize(30,30)) |
|
||||
shadow = QtGui.QIcon(options[status]).pixmap(QtCore.QSize(35,35)) #original color is black |
|
||||
|
|
||||
#Colorize overlay symbol. Painter works inplace. |
|
||||
painter = QtGui.QPainter(overlayPixmap); |
|
||||
painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceIn) |
|
||||
painter.fillRect(overlayPixmap.rect(), QtGui.QColor("cyan")) |
|
||||
painter.end() |
|
||||
|
|
||||
icon = self.parentController.mainWindow.programIcons[self.agordejoExec] |
|
||||
pixmap = icon.pixmap(QtCore.QSize(70,70)) |
|
||||
|
|
||||
p = QtGui.QPainter(pixmap) |
|
||||
|
|
||||
p.drawPixmap(0, -1, shadow) |
|
||||
p.drawPixmap(2, 2, overlayPixmap) #top left corner of icon, with some padding for the shadow |
|
||||
p.end() |
|
||||
ico = QtGui.QIcon(pixmap) |
|
||||
self.setIcon(ico) |
|
||||
else: |
|
||||
if self.agordejoExec in self.parentController.mainWindow.programIcons: #there was a strange bug once where this happened exactly one, and then everything was fine, including this icon. Some DB backwards compatibility. |
|
||||
ico = self.parentController.mainWindow.programIcons[self.agordejoExec] |
|
||||
self.setIcon(ico) |
|
||||
|
|
||||
|
|
||||
#Status |
|
||||
def ready(self): |
|
||||
if self.nsmClientDict["hasOptionalGUI"]: |
|
||||
if self.nsmClientDict["visible"]: |
|
||||
self._setIconOverlay("ready") |
|
||||
else: |
|
||||
self._setIconOverlay("hidden") |
|
||||
else: |
|
||||
self._setIconOverlay("ready") |
|
||||
|
|
||||
#self.setFlags(QtCore.Qt.ItemIsEnabled) |
|
||||
|
|
||||
def removed(self): |
|
||||
#self.setFlags(QtCore.Qt.NoItemFlags) #Black and white. We can still mouseClick through parent signal when set to NoItemFlags |
|
||||
self.nsmClientDict = None #in opposite to stop |
|
||||
|
|
||||
def stopped(self): |
|
||||
self.setFlags(QtCore.Qt.ItemIsEnabled) |
|
||||
self._setIconOverlay("stopped") |
|
||||
|
|
||||
def handleClick(self): |
|
||||
alreadyInSession = api.executableInSession(self.agordejoExec) |
|
||||
#Paranoia Start |
|
||||
if self.nsmClientDict is None and alreadyInSession: |
|
||||
#Caught double-click. do nothing, this is a user-accident |
|
||||
return |
|
||||
elif self.nsmClientDict: |
|
||||
assert alreadyInSession |
|
||||
elif alreadyInSession: |
|
||||
assert self.nsmClientDict |
|
||||
#Paranoia End |
|
||||
|
|
||||
if not alreadyInSession: |
|
||||
api.clientAdd(self.agordejoExec) #triggers status update callback which activates our item. |
|
||||
elif self.nsmClientDict["lastStatus"] == "stopped": |
|
||||
api.clientResume(self.nsmClientDict["clientId"]) |
|
||||
else: |
|
||||
api.clientToggleVisible(self.nsmClientDict["clientId"]) #api is tolerant to sending this to non-optional-GUI clients |
|
||||
|
|
||||
|
|
||||
class QuickOpenSessionController(object): |
|
||||
"""Controls the widget, but does not subclass. |
|
||||
|
|
||||
We want the simplest form of interaction possible: single touch. |
|
||||
No selections, no right click. Like a smartphone app. |
|
||||
""" |
|
||||
|
|
||||
def __init__(self, mainWindow): |
|
||||
iconSize = 70 |
|
||||
self.mainWindow = mainWindow |
|
||||
self.listWidget = mainWindow.ui.quickSessionClientsListWidget |
|
||||
self.listWidget.setIconSize(QtCore.QSize(iconSize,iconSize)) |
|
||||
self.listWidget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) |
|
||||
self.listWidget.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) |
|
||||
self.listWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) #Icons can't be selected. Text still can |
|
||||
self.listWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) |
|
||||
self.listWidget.setResizeMode(QtWidgets.QListView.Adjust) |
|
||||
self.listWidget.setGridSize(QtCore.QSize(iconSize*1.2,iconSize*2)) #x spacing, y for text |
|
||||
self.listWidget.setWordWrap(True) #needed for grid, don't use without grid (i.e. setSparcing and setUniformItemSizes) |
|
||||
#self.listWidget.setSpacing(20) # Grid is better |
|
||||
#self.listWidget.setUniformItemSizes(True) # Grid is better |
|
||||
|
|
||||
|
|
||||
self._nsmSessionExportDict = None |
|
||||
self.nameWidget = mainWindow.ui.quickSessionNameLineEdit |
|
||||
self.layout = mainWindow.ui.page_quickSessionLoaded.layout() |
|
||||
self.clientOverrideNamesCache = None #None or dict. Dict is never truly empty, it has at least empty categories. |
|
||||
self.descriptionController = DescriptionController(mainWindow, self.mainWindow.ui.quickSessionNotesGroupBox, self.mainWindow.ui.quickSessionNotesPlainTextEdit) |
|
||||
|
|
||||
font = self.nameWidget.font() |
|
||||
font.setPixelSize(font.pixelSize() * 1.4) |
|
||||
self.nameWidget.setFont(font) |
|
||||
|
|
||||
#GUI Signals |
|
||||
#self.listWidget.itemActivated.connect(lambda item:print (item)) #Activated is system dependend. On osx it might be single clicke, here on linux is double click. on phones it might be something else. |
|
||||
self.listWidget.itemClicked.connect(self._itemClicked) |
|
||||
|
|
||||
mainWindow.ui.quickCloseOpenSession.clicked.connect(api.sessionClose) |
|
||||
font = mainWindow.ui.quickCloseOpenSession.font() |
|
||||
font.setPixelSize(font.pixelSize() * 1.2) |
|
||||
mainWindow.ui.quickCloseOpenSession.setFont(font) |
|
||||
|
|
||||
mainWindow.ui.quickSaveOpenSession.clicked.connect(api.sessionSave) |
|
||||
mainWindow.ui.quickSaveOpenSession.hide() |
|
||||
|
|
||||
#API Callbacks |
|
||||
api.callbacks.sessionOpenLoading.append(self.buildCleanStarterClients) |
|
||||
api.callbacks.sessionOpenLoading.append(self._openLoading) |
|
||||
api.callbacks.sessionOpenReady.append(self._openReady) |
|
||||
api.callbacks.sessionClosed.append(self._sendNameChange) |
|
||||
api.callbacks.clientStatusChanged.append(self._clientStatusChanged) |
|
||||
|
|
||||
self.listWidget.setFocus() #take focus away from title-edit |
|
||||
|
|
||||
logger.info("Quick Open Session Controller ready") |
|
||||
|
|
||||
def _itemClicked(self, item): |
|
||||
self.listWidget.reset() #Hackity Hack! This is intended to revert the text-selection of items. However, that is not a real selection. clearSelection does nothing! Now it looks like a brief flash. |
|
||||
item.handleClick() |
|
||||
|
|
||||
|
|
||||
|
|
||||
def _openLoading(self, nsmSessionExportDict): |
|
||||
self._nsmSessionExportDict = nsmSessionExportDict |
|
||||
self.nameWidget.setText(nsmSessionExportDict["nsmSessionName"]) |
|
||||
|
|
||||
def _openReady(self, nsmSessionExportDict): |
|
||||
self._nsmSessionExportDict = nsmSessionExportDict |
|
||||
self.nameWidget.setText(nsmSessionExportDict["nsmSessionName"]) |
|
||||
|
|
||||
def _sendNameChange(self): |
|
||||
"""The closed callback is send on start to indicate "no open session". exportDict cache is |
|
||||
not ready then. We need to test. |
|
||||
It is not possible to rename a running session. We allow the user to fake-edit the name |
|
||||
but will only send the api request after the session is closed""" |
|
||||
if not self._nsmSessionExportDict: #see docstring |
|
||||
return |
|
||||
|
|
||||
if self.nameWidget.text() and not self.nameWidget.text() == self._nsmSessionExportDict["nsmSessionName"]: |
|
||||
logger.info(f"Instructing the api to rename session {self._nsmSessionExportDict['nsmSessionName']} to {self.nameWidget.text()} on close") |
|
||||
api.sessionRename(self._nsmSessionExportDict["nsmSessionName"], self.nameWidget.text()) |
|
||||
|
|
||||
self._nsmSessionExportDict = None #now really closed |
|
||||
|
|
||||
def buildCleanStarterClients(self, nsmSessionExportDict:dict): |
|
||||
"""Reset everything to the initial, empty state. |
|
||||
We do not reset in openReady because that signifies that the session is ready. |
|
||||
And not in session closed because we want to setup data structures. |
|
||||
|
|
||||
In comparison with the detailed view open session controller we need to do incremental |
|
||||
updates. The detailed view can just delete and recreater its launchers after a DB-update, |
|
||||
but we combine both views. So we can't just delete-and-rebuild because that destroys |
|
||||
running client states. |
|
||||
""" |
|
||||
engineCache = api.getCache() |
|
||||
programs = engineCache["programs"] |
|
||||
|
|
||||
whitelist = [e for e in programs if e["whitelist"]] |
|
||||
leftovers = set(StarterClientItem.allItems.keys()) #"agordejoExec" |
|
||||
|
|
||||
notForQuickView = ("nsm-data", "jackpatch", "nsm-proxy", "non-midi-mapper", "non-mixer-noui", "ray-proxy", "ray-jackpatch", "carla-jack-single", "carla-jack-multi") |
|
||||
|
|
||||
for forIcon in StarterClientItem.allItems.values(): |
|
||||
forIcon._setIconOverlay("") #empty initial state |
|
||||
|
|
||||
for entry in whitelist: |
|
||||
exe = entry["agordejoExec"] |
|
||||
if exe in StarterClientItem.allItems: |
|
||||
if entry["agordejoExec"] in leftovers: #It happened that it was not. Don't ask me... |
|
||||
leftovers.remove(entry["agordejoExec"]) |
|
||||
else: |
|
||||
#Create new. Item will be parented by Qt, so Python GC will not delete |
|
||||
if not exe in notForQuickView: |
|
||||
item = StarterClientItem(parentController=self, desktopEntry=entry) |
|
||||
self.listWidget.addItem(item) |
|
||||
StarterClientItem.allItems[entry["agordejoExec"]] = item |
|
||||
|
|
||||
#Remove starters that were available until they got removed in the last db update |
|
||||
for loexe in leftovers: |
|
||||
item = StarterClientItem.allItems[loexe] |
|
||||
del StarterClientItem.allItems[loexe] |
|
||||
index = self.listWidget.indexFromItem(item).row() #Row is the real index in a listView, no matter iconViewMode. |
|
||||
self.listWidget.takeItem(index) |
|
||||
del item |
|
||||
|
|
||||
|
|
||||
def _clientStatusChanged(self, clientDict:dict): |
|
||||
"""Maps to nsmd status changes. |
|
||||
We already have icons for all programs, in opposite to detailed-view opensession controller. |
|
||||
Status updates are used to switch them on an off. |
|
||||
We also present only one icon per executable. If you want more go into the other mode. |
|
||||
""" |
|
||||
#index = self.listWidget.indexFromItem(QuickClientItem.allItems[clientId]).row() #Row is the real index in a listView, no matter iconViewMode. |
|
||||
|
|
||||
assert clientDict["executable"] |
|
||||
|
|
||||
if clientDict["dumbClient"]: #only real nsm clients in our session, whic includes the initial "not-yet" status of nsm-clients. |
|
||||
return |
|
||||
backgroundClients = METADATA["preferredClients"].values() |
|
||||
if clientDict["executable"] in backgroundClients: |
|
||||
return |
|
||||
|
|
||||
if clientDict["executable"] in StarterClientItem.allItems: |
|
||||
item = StarterClientItem.allItems[clientDict["executable"]] |
|
||||
item.updateStatus(clientDict) |
|
||||
else: |
|
||||
logging.warning(f"Got client status update for {clientDict['executable']}, which is not in our database. This can happen if you install a program and do not update the DB. Please do so and then restart the session.") |
|
@ -1,127 +0,0 @@ |
|||||
#! /usr/bin/env python3 |
|
||||
# -*- coding: utf-8 -*- |
|
||||
""" |
|
||||
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) |
|
||||
|
|
||||
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). |
|
||||
|
|
||||
This application is free software: you can redistribute it and/or modify |
|
||||
it under the terms of the GNU General Public License as published by |
|
||||
the Free Software Foundation, either version 3 of the License, or |
|
||||
(at your option) any later version. |
|
||||
|
|
||||
This program is distributed in the hope that it will be useful, |
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
GNU General Public License for more details. |
|
||||
|
|
||||
You should have received a copy of the GNU General Public License |
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
||||
""" |
|
||||
|
|
||||
import logging; logger = logging.getLogger(__name__); logger.info("import") |
|
||||
|
|
||||
#Standard Library |
|
||||
import datetime |
|
||||
|
|
||||
#Third Party |
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets |
|
||||
|
|
||||
#Engine |
|
||||
from engine.config import METADATA #includes METADATA only. No other environmental setup is executed. |
|
||||
import engine.api as api |
|
||||
|
|
||||
|
|
||||
|
|
||||
class SessionButton(QtWidgets.QPushButton): |
|
||||
|
|
||||
def __init__(self, sessionDict): |
|
||||
self.sessionDict = sessionDict |
|
||||
super().__init__(sessionDict["nsmSessionName"]) |
|
||||
self.clicked.connect(self.openSession) |
|
||||
#self.setFlat(True) |
|
||||
|
|
||||
font = self.font() |
|
||||
font.setPixelSize(font.pixelSize() * 1.2 ) |
|
||||
self.setFont(font) |
|
||||
|
|
||||
#width = self.fontMetrics().boundingRect(sessionDict["nsmSessionName"]).width()+10 |
|
||||
#width = self.fontMetrics().boundingRect(longestSessionName).width()+10 |
|
||||
#width = parent.geometry().width() |
|
||||
#self.setFixedSize(width, 40) |
|
||||
self.setFixedHeight(40) |
|
||||
|
|
||||
|
|
||||
def openSession(self): |
|
||||
name = self.sessionDict["nsmSessionName"] |
|
||||
api.sessionOpen(name) |
|
||||
|
|
||||
class QuickSessionController(object): |
|
||||
"""Controls the widget, but does not subclass""" |
|
||||
|
|
||||
def __init__(self, mainWindow): |
|
||||
self.mainWindow = mainWindow |
|
||||
self.layout = mainWindow.ui.quickSessionChooser.layout() |
|
||||
#self.layout.setAlignment(QtCore.Qt.AlignHCenter) |
|
||||
newSessionButton = mainWindow.ui.quickNewSession |
|
||||
|
|
||||
font = newSessionButton.font() |
|
||||
font.setPixelSize(font.pixelSize() * 1.4) |
|
||||
newSessionButton.setFont(font) |
|
||||
|
|
||||
newSessionButton.setFixedHeight(40) |
|
||||
newSessionButton.setFocus(True) #Enter on program start creates a new session. |
|
||||
newSessionButton.clicked.connect(self._newTimestampSession) |
|
||||
api.callbacks.sessionsChanged.append(self._reactCallback_sessionsChanged) |
|
||||
|
|
||||
#self.layout.geometry().width() #very small |
|
||||
#self.mainWindow.ui.quickSessionChooser.geometry().width() #too small |
|
||||
#self.mainWindow.ui.scrollArea.geometry().width() |
|
||||
#mainWindow.geometry().width() |
|
||||
|
|
||||
|
|
||||
logger.info("Quick Session Chooser ready") |
|
||||
|
|
||||
def _clear(self): |
|
||||
"""Clear everything but the spacer item""" |
|
||||
for child in self.mainWindow.ui.quickSessionChooser.children(): |
|
||||
if type(child) is SessionButton: |
|
||||
self.layout.removeWidget(child) |
|
||||
child.setParent(None) |
|
||||
del child |
|
||||
|
|
||||
|
|
||||
|
|
||||
def _reactCallback_sessionsChanged(self, sessionDicts:list): |
|
||||
"""Main callback for new, added, removed, moved sessions etc.""" |
|
||||
logger.info("Rebuilding session buttons") |
|
||||
self._clear() #except the space |
|
||||
|
|
||||
spacer = self.layout.takeAt(0) |
|
||||
|
|
||||
#longestSessionName = "" |
|
||||
#for sessionDict in sessionDicts: |
|
||||
# if len(sessionDict["nsmSessionName"]) > len(longestSessionName): |
|
||||
# longestSessionName = sessionDict["nsmSessionName"] |
|
||||
|
|
||||
for sessionDict in sorted(sessionDicts, key=lambda d: d["nsmSessionName"]): |
|
||||
self.layout.addWidget(SessionButton(sessionDict)) |
|
||||
|
|
||||
#Finally add vertical spacer |
|
||||
#spacerItem = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) #int w, int h, QSizePolicy::Policy hPolicy = QSizePolicy::Minimum, QSizePolicy::Policy vPolicy = QSizePolicy::Minimum |
|
||||
self.layout.addItem(spacer) |
|
||||
|
|
||||
def _newTimestampSession(self): |
|
||||
nsmExecutables = api.getNsmExecutables() #type set, cached, very fast. |
|
||||
con = METADATA["preferredClients"]["data"] |
|
||||
data = METADATA["preferredClients"]["connections"] |
|
||||
startclients = [] |
|
||||
if con in nsmExecutables: |
|
||||
startclients.append(con) |
|
||||
if data in nsmExecutables: |
|
||||
startclients.append(data) |
|
||||
|
|
||||
#now = datetime.datetime.now().replace(second=0, microsecond=0).isoformat()[:-3] |
|
||||
now = datetime.datetime.now().replace(microsecond=0).isoformat() |
|
||||
name = now |
|
||||
api.sessionNew(name, startclients) |
|
Loading…
Reference in new issue