Commit b1968330937a12ef804a4fffed6a3b3a1a8a6c34

Authored by Renan
2 parents 0065134e 9460b9cd
Exists in master

Merge branch 'master' into multimodal_tracking

# Conflicts:
#	invesalius/gui/task_navigator.py
invesalius/gui/dialogs.py
... ... @@ -3554,8 +3554,6 @@ class ObjectCalibrationDialog(wx.Dialog):
3554 3554 def set_fiducial_callback(state):
3555 3555 if state:
3556 3556 Publisher.sendMessage('Set object fiducial', fiducial_index=index)
3557   - if self.pedal_connection is not None:
3558   - self.pedal_connection.remove_callback('fiducial')
3559 3557  
3560 3558 ctrl.SetValue(False)
3561 3559 self.object_fiducial_being_set = None
... ... @@ -3564,10 +3562,17 @@ class ObjectCalibrationDialog(wx.Dialog):
3564 3562 self.object_fiducial_being_set = index
3565 3563  
3566 3564 if self.pedal_connection is not None:
3567   - self.pedal_connection.add_callback('fiducial', set_fiducial_callback)
  3565 + self.pedal_connection.add_callback(
  3566 + name='fiducial',
  3567 + callback=set_fiducial_callback,
  3568 + remove_when_released=True,
  3569 + )
3568 3570 else:
3569 3571 set_fiducial_callback(True)
3570 3572  
  3573 + if self.pedal_connection is not None:
  3574 + self.pedal_connection.remove_callback(name='fiducial')
  3575 +
3571 3576 def SetObjectFiducial(self, fiducial_index):
3572 3577 coord, coord_raw = self.tracker.GetTrackerCoordinates(
3573 3578 # XXX: Always use static reference mode when getting the coordinates. This is what the
... ...
invesalius/gui/task_navigator.py
... ... @@ -219,6 +219,13 @@ class InnerFoldPanel(wx.Panel):
219 219 leftSpacing=0, rightSpacing=0)
220 220 self.dbs_item.Hide()
221 221  
  222 + # Fold 6 - Sessions
  223 + item = fold_panel.AddFoldPanel(_("Sessions"), collapsed=False)
  224 + stw = SessionPanel(item)
  225 + fold_panel.ApplyCaptionStyle(item, style)
  226 + fold_panel.AddFoldPanelWindow(item, stw, spacing= 0,
  227 + leftSpacing=0, rightSpacing=0)
  228 +
222 229 # Check box for camera update in volume rendering during navigation
223 230 tooltip = wx.ToolTip(_("Update camera in volume"))
224 231 checkcamera = wx.CheckBox(self, -1, _('Vol. camera'))
... ... @@ -437,7 +444,7 @@ class NeuronavigationPanel(wx.Panel):
437 444 checkbox_pedal_pressed.Enable(False)
438 445 checkbox_pedal_pressed.SetToolTip(tooltip)
439 446  
440   - pedal_connection.add_callback('gui', checkbox_pedal_pressed.SetValue)
  447 + pedal_connection.add_callback(name='gui', callback=checkbox_pedal_pressed.SetValue)
441 448  
442 449 self.checkbox_pedal_pressed = checkbox_pedal_pressed
443 450 else:
... ... @@ -705,19 +712,25 @@ class NeuronavigationPanel(wx.Panel):
705 712 if state:
706 713 fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name']
707 714 Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name)
708   - if self.pedal_connection is not None:
709   - self.pedal_connection.remove_callback('fiducial')
710 715  
711 716 ctrl.SetValue(False)
712 717 self.tracker_fiducial_being_set = None
713 718  
714 719 if ctrl.GetValue():
715 720 self.tracker_fiducial_being_set = n
  721 +
716 722 if self.pedal_connection is not None:
717   - self.pedal_connection.add_callback('fiducial', set_fiducial_callback)
  723 + self.pedal_connection.add_callback(
  724 + name='fiducial',
  725 + callback=set_fiducial_callback,
  726 + remove_when_released=True,
  727 + )
718 728 else:
719 729 set_fiducial_callback(True)
720 730  
  731 + if self.pedal_connection is not None:
  732 + self.pedal_connection.remove_callback(name='fiducial')
  733 +
721 734 def OnStopNavigation(self):
722 735 select_tracker_elem = self.select_tracker_elem
723 736 choice_ref = self.choice_ref
... ... @@ -1128,8 +1141,8 @@ class MarkersPanel(wx.Panel):
1128 1141 alpha_robot : float = 0
1129 1142 beta_robot: float = 0
1130 1143 gamma_robot : float = 0
1131   - is_target : int = 0 # is_target is int instead of boolean to avoid
1132   - # problems with CSV export
  1144 + is_target : bool = False
  1145 + session_id : int = 1
1133 1146  
1134 1147 # x, y, z, alpha, beta, gamma can be jointly accessed as coord
1135 1148 @property
... ... @@ -1177,27 +1190,44 @@ class MarkersPanel(wx.Panel):
1177 1190 self.x_robot, self.y_robot, self.z_robot, self.alpha_robot, self.beta_robot, self.gamma_robot = new_robot
1178 1191  
1179 1192 @classmethod
1180   - def get_headers(cls):
1181   - """Return the list of field names (headers) for exporting to csv."""
  1193 + def to_string_headers(cls):
  1194 + """Return the string containing tab-separated list of field names (headers)."""
1182 1195 res = [field.name for field in dataclasses.fields(cls)]
1183 1196 res.extend(['x_world', 'y_world', 'z_world', 'alpha_world', 'beta_world', 'gamma_world'])
1184   - return res
  1197 + return '\t'.join(map(lambda x: '\"%s\"' % x, res))
1185 1198  
1186   - def get_values(self):
1187   - """Return the list of values for exporting to csv."""
1188   - res = []
1189   - res.extend(dataclasses.astuple(self))
  1199 + def to_string(self):
  1200 + """Serialize to excel-friendly tab-separated string"""
  1201 + res = ''
  1202 + for field in dataclasses.fields(self.__class__):
  1203 + if field.type is str:
  1204 + res += ('\"%s\"\t' % getattr(self, field.name))
  1205 + else:
  1206 + res += ('%s\t' % str(getattr(self, field.name)))
1190 1207  
1191 1208 # Add world coordinates (in addition to the internal ones).
1192 1209 position_world, orientation_world = imagedata_utils.convert_invesalius_to_world(
1193 1210 position=[self.x, self.y, self.z],
1194 1211 orientation=[self.alpha, self.beta, self.gamma],
1195 1212 )
1196   - res.extend(position_world)
1197   - res.extend(orientation_world)
1198 1213  
  1214 + res += '\t'.join(map(lambda x: 'N/A' if x is None else str(x), (*position_world, *orientation_world)))
1199 1215 return res
1200 1216  
  1217 + def from_string(self, str):
  1218 + """Deserialize from a tab-separated string. If the string is not
  1219 + properly formatted, might throw an exception and leave the object
  1220 + in an inconsistent state."""
  1221 + for field, str_val in zip(dataclasses.fields(self.__class__), str.split('\t')):
  1222 + if field.type is float:
  1223 + setattr(self, field.name, float(str_val))
  1224 + if field.type is int:
  1225 + setattr(self, field.name, int(str_val))
  1226 + if field.type is str:
  1227 + setattr(self, field.name, str_val[1:-1]) # remove the quotation marks
  1228 + if field.type is bool:
  1229 + setattr(self, field.name, str_val=='True')
  1230 +
1201 1231 def __init__(self, parent, tracker):
1202 1232 wx.Panel.__init__(self, parent)
1203 1233 try:
... ... @@ -1218,18 +1248,11 @@ class MarkersPanel(wx.Panel):
1218 1248 self.markers = []
1219 1249 self.current_head = 0, 0, 0, 0, 0, 0
1220 1250 self.current_robot = 0, 0, 0, 0, 0, 0
1221   - self.list_coord = []
1222   - self.marker_ind = 0
1223   - self.tgt_flag = self.tgt_index = None
1224 1251 self.nav_status = False
1225   - self.mchange = None
1226   - self.flag_target = False
1227 1252  
1228 1253 self.marker_colour = const.MARKER_COLOUR
1229 1254 self.marker_size = const.MARKER_SIZE
1230   -
1231   - # Define CSV dialect for saving/loading markers
1232   - csv.register_dialect('markers_dialect', delimiter='\t', quoting=csv.QUOTE_NONNUMERIC)
  1255 + self.current_session = 1
1233 1256  
1234 1257 # Change marker size
1235 1258 spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23))
... ... @@ -1284,6 +1307,7 @@ class MarkersPanel(wx.Panel):
1284 1307 self.lc.InsertColumn(3, 'Z')
1285 1308 self.lc.InsertColumn(4, 'ID')
1286 1309 self.lc.InsertColumn(5, 'Target')
  1310 + self.lc.InsertColumn(6, 'Session')
1287 1311  
1288 1312 self.lc.SetColumnWidth(0, 28)
1289 1313 self.lc.SetColumnWidth(1, 50)
... ... @@ -1291,6 +1315,7 @@ class MarkersPanel(wx.Panel):
1291 1315 self.lc.SetColumnWidth(3, 50)
1292 1316 self.lc.SetColumnWidth(4, 60)
1293 1317 self.lc.SetColumnWidth(5, 60)
  1318 + self.lc.SetColumnWidth(5, 50)
1294 1319  
1295 1320 self.lc.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown)
1296 1321 self.lc.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink)
... ... @@ -1315,11 +1340,8 @@ class MarkersPanel(wx.Panel):
1315 1340 Publisher.subscribe(self.CreateMarker, 'Create marker')
1316 1341 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
1317 1342 Publisher.subscribe(self.UpdateSeedCoordinates, 'Update tracts')
1318   - Publisher.subscribe(self.UpdateMchange, 'Update matrix change')
1319   - Publisher.subscribe(self.UpdateMRef, 'Update ref matrix')
  1343 + Publisher.subscribe(self.OnChangeCurrentSession, 'Current session changed')
1320 1344 Publisher.subscribe(self.UpdateRobotCoord, 'Update raw coord')
1321   - Publisher.subscribe(self.UpdateObjectMarker2Center, 'Update object marker to center')
1322   - Publisher.subscribe(self.OnObjectTarget, 'Coil at target')
1323 1345  
1324 1346 def __find_target_marker(self):
1325 1347 """Return the index of the marker currently selected as target (there
... ... @@ -1327,17 +1349,17 @@ class MarkersPanel(wx.Panel):
1327 1349 for i in range(len(self.markers)):
1328 1350 if self.markers[i].is_target:
1329 1351 return i
1330   -
  1352 +
1331 1353 return -1
1332 1354  
1333 1355 def __get_selected_items(self):
1334   - """
  1356 + """
1335 1357 Returns a (possibly empty) list of the selected items in the list control.
1336 1358 """
1337 1359 selection = []
1338 1360  
1339 1361 next = self.lc.GetFirstSelected()
1340   -
  1362 +
1341 1363 while next != -1:
1342 1364 selection.append(next)
1343 1365 next = self.lc.GetNextSelected(next)
... ... @@ -1366,13 +1388,13 @@ class MarkersPanel(wx.Panel):
1366 1388  
1367 1389 # Unset the previous target
1368 1390 if prev_idx != -1:
1369   - self.markers[prev_idx].is_target = 0
  1391 + self.markers[prev_idx].is_target = False
1370 1392 self.lc.SetItemBackgroundColour(prev_idx, 'white')
1371 1393 Publisher.sendMessage('Set target transparency', status=False, index=prev_idx)
1372 1394 self.lc.SetItem(prev_idx, 5, "")
1373 1395  
1374 1396 # Set the new target
1375   - self.markers[idx].is_target = 1
  1397 + self.markers[idx].is_target = True
1376 1398 self.lc.SetItemBackgroundColour(idx, 'RED')
1377 1399 self.lc.SetItem(idx, 5, _("Yes"))
1378 1400  
... ... @@ -1400,27 +1422,14 @@ class MarkersPanel(wx.Panel):
1400 1422 def UpdateSeedCoordinates(self, root=None, affine_vtk=None, coord_offset=(0, 0, 0)):
1401 1423 self.current_seed = coord_offset
1402 1424  
1403   - def UpdateMRef(self, m_ref):
1404   - self.m_ref = m_ref
1405   -
1406   - def UpdateMchange(self, mchange):
1407   - self.mchange = mchange
1408   -
1409 1425 def UpdateRobotCoord(self, coord_raw, markers_flag):
1410 1426 self.current_head = coord_raw[1]
1411 1427 self.current_robot = coord_raw[2]
1412 1428  
1413   - def UpdateObjectMarker2Center(self, s0_raw, t_offset):
1414   - self.s0_raw=s0_raw
1415   - self.t_offset=t_offset
1416   -
1417   - def OnObjectTarget(self, state):
1418   - self.flag_target = state
1419   -
1420 1429 def OnMouseRightDown(self, evt):
1421 1430 # TODO: Enable the "Set as target" only when target is created with registered object
1422 1431 menu_id = wx.Menu()
1423   - edit_id = menu_id.Append(0, _('Edit ID'))
  1432 + edit_id = menu_id.Append(0, _('Edit label'))
1424 1433 menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerLabel, edit_id)
1425 1434 color_id = menu_id.Append(2, _('Edit color'))
1426 1435 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id)
... ... @@ -1530,7 +1539,7 @@ class MarkersPanel(wx.Panel):
1530 1539  
1531 1540 if not evt: # called through pubsub
1532 1541 index = []
1533   -
  1542 +
1534 1543 if label and (label in self.__list_fiducial_labels()):
1535 1544 for id_n in range(self.lc.GetItemCount()):
1536 1545 item = self.lc.GetItem(id_n, 4)
... ... @@ -1544,14 +1553,13 @@ class MarkersPanel(wx.Panel):
1544 1553 if index:
1545 1554 if self.__find_target_marker() in index:
1546 1555 Publisher.sendMessage('Disable or enable coil tracker', status=False)
  1556 + Publisher.sendMessage('Robot target matrix', robot_tracker_flag=False,
  1557 + m_change_robot2ref=None)
1547 1558 wx.MessageBox(_("Target deleted."), _("InVesalius 3"))
1548 1559  
1549 1560 self.__delete_multiple_markers(index)
1550 1561 else:
1551 1562 if evt: # Don't show the warning if called through pubsub
1552   - #TODO: reset robot target. (target should be the same target as invesalius?)
1553   - Publisher.sendMessage('Robot target matrix', robot_tracker_flag=False,
1554   - m_change_robot2ref=None)
1555 1563 wx.MessageBox(_("No data selected."), _("InVesalius 3"))
1556 1564  
1557 1565 def OnCreateMarker(self, evt):
... ... @@ -1563,10 +1571,10 @@ class MarkersPanel(wx.Panel):
1563 1571 file should not contain any fiducials already in the list."""
1564 1572 filename = dlg.ShowLoadSaveDialog(message=_(u"Load markers"),
1565 1573 wildcard=const.WILDCARD_MARKER_FILES)
1566   -
  1574 +
1567 1575 if not filename:
1568 1576 return
1569   -
  1577 +
1570 1578 try:
1571 1579 with open(filename, 'r') as file:
1572 1580 magick_line = file.readline()
... ... @@ -1575,28 +1583,29 @@ class MarkersPanel(wx.Panel):
1575 1583 if ver != 0:
1576 1584 wx.MessageBox(_("Unknown version of the markers file."), _("InVesalius 3"))
1577 1585 return
1578   -
1579   - reader = csv.reader(file, dialect='markers_dialect')
1580   - next(reader) # skip the header line
  1586 +
  1587 + file.readline() # skip the header line
1581 1588  
1582 1589 # Read the data lines and create markers
1583   - for line in reader:
1584   - marker = self.Marker(*line[:-6]) # Discard the last 6 fields (the world coordinates)
  1590 + for line in file.readlines():
  1591 + marker = self.Marker()
  1592 + marker.from_string(line)
1585 1593 self.CreateMarker(coord=marker.coord, colour=marker.colour, size=marker.size,
1586   - label=marker.label, is_target=0, seed=marker.seed)
  1594 + label=marker.label, is_target=False, seed=marker.seed, session_id=marker.session_id)
1587 1595  
1588 1596 if marker.label in self.__list_fiducial_labels():
1589 1597 Publisher.sendMessage('Load image fiducials', label=marker.label, coord=marker.coord)
1590 1598  
1591   - # If the new marker has is_target=1 (True), we first create
1592   - # a marker with is_target=0 (False), and then call __set_marker_as_target
  1599 + # If the new marker has is_target=True, we first create
  1600 + # a marker with is_target=False, and then call __set_marker_as_target
1593 1601 if marker.is_target:
1594 1602 self.__set_marker_as_target(len(self.markers)-1)
1595 1603  
1596 1604 except:
1597   - wx.MessageBox(_("Invalid markers file."), _("InVesalius 3"))
  1605 + wx.MessageBox(_("Invalid markers file."), _("InVesalius 3"))
1598 1606  
1599 1607 def OnMarkersVisibility(self, evt, ctrl):
  1608 +
1600 1609 if ctrl.GetValue():
1601 1610 Publisher.sendMessage('Hide all markers', indexes=self.lc.GetItemCount())
1602 1611 ctrl.SetLabel('Show')
... ... @@ -1624,12 +1633,11 @@ class MarkersPanel(wx.Panel):
1624 1633 try:
1625 1634 with open(filename, 'w', newline='') as file:
1626 1635 file.writelines(['%s%i\n' % (const.MARKER_FILE_MAGICK_STRING, const.CURRENT_MARKER_FILE_VERSION)])
1627   - writer = csv.writer(file, dialect='markers_dialect')
1628   - writer.writerow(self.Marker.get_headers())
1629   - writer.writerows(marker.get_values() for marker in self.markers)
  1636 + file.writelines(['%s\n' % self.Marker.to_string_headers()])
  1637 + file.writelines('%s\n' % marker.to_string() for marker in self.markers)
1630 1638 file.close()
1631 1639 except:
1632   - wx.MessageBox(_("Error writing markers file."), _("InVesalius 3"))
  1640 + wx.MessageBox(_("Error writing markers file."), _("InVesalius 3"))
1633 1641  
1634 1642 def OnSelectColour(self, evt, ctrl):
1635 1643 #TODO: Make sure GetValue returns 3 numbers (without alpha)
... ... @@ -1638,7 +1646,10 @@ class MarkersPanel(wx.Panel):
1638 1646 def OnSelectSize(self, evt, ctrl):
1639 1647 self.marker_size = ctrl.GetValue()
1640 1648  
1641   - def CreateMarker(self, coord=None, colour=None, size=None, label='*', is_target=0, seed=None, head=None, robot=None):
  1649 + def OnChangeCurrentSession(self, new_session_id):
  1650 + self.current_session = new_session_id
  1651 +
  1652 + def CreateMarker(self, coord=None, colour=None, size=None, label='*', is_target=0, seed=None, head=None, robot=None, session_id=None):
1642 1653 new_marker = self.Marker()
1643 1654 new_marker.coord = coord or self.current_coord
1644 1655 new_marker.colour = colour or self.marker_colour
... ... @@ -1648,6 +1659,7 @@ class MarkersPanel(wx.Panel):
1648 1659 new_marker.seed = seed or self.current_seed
1649 1660 new_marker.head = head or self.current_head
1650 1661 new_marker.robot = robot or self.current_robot
  1662 + new_marker.session_id = session_id or self.current_session
1651 1663  
1652 1664 # Note that ball_id is zero-based, so we assign it len(self.markers) before the new marker is added
1653 1665 Publisher.sendMessage('Add marker', ball_id=len(self.markers),
... ... @@ -1662,7 +1674,8 @@ class MarkersPanel(wx.Panel):
1662 1674 self.lc.SetItem(num_items, 1, str(round(new_marker.x, 2)))
1663 1675 self.lc.SetItem(num_items, 2, str(round(new_marker.y, 2)))
1664 1676 self.lc.SetItem(num_items, 3, str(round(new_marker.z, 2)))
1665   - self.lc.SetItem(num_items, 4, str(new_marker.label))
  1677 + self.lc.SetItem(num_items, 4, new_marker.label)
  1678 + self.lc.SetItem(num_items, 6, str(new_marker.session_id))
1666 1679 self.lc.EnsureVisible(num_items)
1667 1680  
1668 1681 class DbsPanel(wx.Panel):
... ... @@ -2116,6 +2129,30 @@ class TractographyPanel(wx.Panel):
2116 2129 Publisher.sendMessage('Remove tracts')
2117 2130  
2118 2131  
  2132 +class SessionPanel(wx.Panel):
  2133 + def __init__(self, parent):
  2134 + wx.Panel.__init__(self, parent)
  2135 + try:
  2136 + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
  2137 + except AttributeError:
  2138 + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR)
  2139 + self.SetBackgroundColour(default_colour)
  2140 +
  2141 + # session count spinner
  2142 + self.__spin_session = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23))
  2143 + self.__spin_session.SetRange(1, 99)
  2144 + self.__spin_session.SetValue(1)
  2145 +
  2146 + self.__spin_session.Bind(wx.EVT_TEXT, self.OnSessionChanged)
  2147 + self.__spin_session.Bind(wx.EVT_SPINCTRL, self.OnSessionChanged)
  2148 +
  2149 + sizer_create = wx.FlexGridSizer(rows=1, cols=1, hgap=5, vgap=5)
  2150 + sizer_create.AddMany([(self.__spin_session, 1)])
  2151 +
  2152 + def OnSessionChanged(self, evt):
  2153 + Publisher.sendMessage('Current session changed', new_session_id=self.__spin_session.GetValue())
  2154 +
  2155 +
2119 2156 class InputAttributes(object):
2120 2157 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python
2121 2158 def __init__(self, *initial_data, **kwargs):
... ...
invesalius/navigation/navigation.py
... ... @@ -316,20 +316,21 @@ class Navigation():
316 316 # del jobs
317 317  
318 318 if self.pedal_connection is not None:
319   - self.pedal_connection.add_callback('navigation', self.PedalStateChanged)
  319 + self.pedal_connection.add_callback(name='navigation', callback=self.PedalStateChanged)
320 320  
321 321 def StopNavigation(self):
322 322 self.event.set()
323 323  
324 324 if self.pedal_connection is not None:
325   - self.pedal_connection.remove_callback('navigation')
  325 + self.pedal_connection.remove_callback(name='navigation')
326 326  
327 327 self.coord_queue.clear()
328 328 self.coord_queue.join()
329 329  
330   - if self.SerialPortEnabled():
  330 + if self.serial_port_connection is not None:
331 331 self.serial_port_connection.join()
332 332  
  333 + if self.SerialPortEnabled():
333 334 self.serial_port_queue.clear()
334 335 self.serial_port_queue.join()
335 336  
... ...
invesalius/net/pedal_connection.py
... ... @@ -40,7 +40,7 @@ class PedalConnection(Thread, metaclass=Singleton):
40 40  
41 41 self._midi_in = None
42 42 self._active_inputs = None
43   - self._callbacks = {}
  43 + self._callback_infos = []
44 44  
45 45 def _midi_to_pedal(self, msg):
46 46 # TODO: At this stage, interpret all note_on messages as the pedal being pressed,
... ... @@ -48,24 +48,22 @@ class PedalConnection(Thread, metaclass=Singleton):
48 48 # message types and be more stringent about the messages.
49 49 #
50 50 if msg.type == 'note_on':
51   - if not self._callbacks:
52   - print("Pedal pressed, no callbacks registered")
53   - else:
54   - Publisher.sendMessage('Pedal state changed', state=True)
55   -
56   - for callback in self._callbacks.values():
57   - callback(True)
  51 + state = True
58 52  
59 53 elif msg.type == 'note_off':
60   - if not self._callbacks:
61   - print("Pedal released, no callbacks registered")
62   - else:
63   - Publisher.sendMessage('Pedal state changed', state=False)
  54 + state = False
64 55  
65   - for callback in self._callbacks.values():
66   - callback(False)
67 56 else:
68 57 print("Unknown message type received from MIDI device")
  58 + return
  59 +
  60 + Publisher.sendMessage('Pedal state changed', state=state)
  61 + for callback_info in self._callback_infos:
  62 + callback = callback_info['callback']
  63 + callback(state)
  64 +
  65 + if not state:
  66 + self._callback_infos = [callback_info for callback_info in self._callback_infos if not callback_info['remove_when_released']]
69 67  
70 68 def _connect_if_disconnected(self):
71 69 if self._midi_in is None and len(self._midi_inputs) > 0:
... ... @@ -93,12 +91,15 @@ class PedalConnection(Thread, metaclass=Singleton):
93 91 def is_connected(self):
94 92 return self._midi_in is not None
95 93  
96   - def add_callback(self, name, callback):
97   - self._callbacks[name] = callback
  94 + def add_callback(self, name, callback, remove_when_released=False):
  95 + self._callback_infos.append({
  96 + 'name': name,
  97 + 'callback': callback,
  98 + 'remove_when_released': remove_when_released,
  99 + })
98 100  
99 101 def remove_callback(self, name):
100   - if name in self._callbacks:
101   - del self._callbacks[name]
  102 + self._callback_infos = [callback_info for callback_info in self._callback_infos if callback_info['name'] != name]
102 103  
103 104 def run(self):
104 105 self.in_use = True
... ...