mediamodel.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. # $Id$
  2. from PyQt4 import QtCore
  3. from bisect import bisect
  4. from openmsx_utils import tclEscape, EscapedStr
  5. import os.path
  6. from qt_utils import QtSignal, Signal
  7. class Medium(QtCore.QObject):
  8. '''All data that belongs to a medium is centralized in this class.
  9. '''
  10. @staticmethod
  11. def create(mediumType, path, patchList = None, mapperType = 'Auto Detect'):
  12. '''Factory method to create the proper Medium instance.
  13. The "mediumType" argument can be a media slot name, as long as it starts
  14. with one of the media type names.
  15. '''
  16. if patchList is None:
  17. patchList = []
  18. if mediumType.startswith('cart'):
  19. return CartridgeMedium(path, patchList, str(mapperType))
  20. elif mediumType.startswith('cassette'):
  21. return CassetteMedium(path)
  22. elif mediumType.startswith('disk'):
  23. return DiskMedium(path, patchList)
  24. else:
  25. return Medium(path)
  26. def __init__(self, path):
  27. QtCore.QObject.__init__(self)
  28. self.__path = path
  29. def getPath(self):
  30. return self.__path
  31. def __eq__(self, other):
  32. # pylint: disable-msg=W0212
  33. return isinstance(other, Medium) and self.__path == other.__path
  34. def __ne__(self, other):
  35. return not self.__eq__(other)
  36. def __str__(self):
  37. return 'medium with path %s' % self.__path
  38. class PatchableMedium(Medium):
  39. '''Abstract base class for patchable media.
  40. '''
  41. def __init__(self, path, patchList):
  42. Medium.__init__(self, path)
  43. self._ipsPatchList = patchList
  44. def copyWithNewPatchList(self, patchList):
  45. return PatchableMedium(self.getPath(), patchList)
  46. def getIpsPatchList(self):
  47. return self._ipsPatchList
  48. def __eq__(self, other):
  49. # pylint: disable-msg=W0212
  50. return (
  51. isinstance(other, PatchableMedium) and
  52. Medium.__eq__(self, other) and
  53. self._ipsPatchList == other._ipsPatchList
  54. )
  55. def __str__(self):
  56. return Medium.__str__(self) + ' and %d patches' \
  57. % len(self._ipsPatchList)
  58. class CartridgeMedium(PatchableMedium):
  59. def __init__(self, path, patchList, mapperType):
  60. PatchableMedium.__init__(self, path, patchList)
  61. self.__mapperType = mapperType
  62. def copyWithNewPatchList(self, patchList):
  63. return CartridgeMedium(self.getPath(), patchList, self.__mapperType)
  64. def getMapperType(self):
  65. return self.__mapperType
  66. def __eq__(self, other):
  67. # pylint: disable-msg=W0212
  68. return (
  69. isinstance(other, CartridgeMedium) and
  70. PatchableMedium.__eq__(self, other) and
  71. self.__mapperType == other.__mapperType
  72. )
  73. def __str__(self):
  74. return 'cartridge' + PatchableMedium.__str__(self) + \
  75. ' and mapper type ' + self.__mapperType
  76. class DiskMedium(PatchableMedium):
  77. def __str__(self):
  78. return 'disk' + PatchableMedium.__str__(self)
  79. class CassetteMedium(Medium):
  80. def __init__(self, path):
  81. Medium.__init__(self, path)
  82. self.__length = 0
  83. def getLength(self):
  84. return self.__length
  85. def setLength(self, length):
  86. '''Only call this from CassetteDeck!'''
  87. self.__length = length
  88. def __str__(self):
  89. return 'cassette' + Medium.__str__(self)
  90. class MediaSlot(QtCore.QObject):
  91. slotDataChanged = Signal('PyQt_PyObject') # slot
  92. @staticmethod
  93. def create(slotName, bridge):
  94. '''Factory method to create the proper MediaSlot instance.
  95. slotName a mediaSlot name, which will also be stored in the object.
  96. '''
  97. if slotName.startswith('cassette'):
  98. return CassetteDeck(slotName, bridge)
  99. elif slotName.startswith('cart'):
  100. return CartridgeSlot(slotName, bridge)
  101. elif slotName.startswith('disk'):
  102. return DiskDrive(slotName, bridge)
  103. else:
  104. return MediaSlot(slotName, bridge)
  105. def __init__(self, name, bridge):
  106. QtCore.QObject.__init__(self)
  107. self.__name = name
  108. self._bridge = bridge
  109. self._medium = None # empty slot
  110. self.__queryMedium()
  111. def __queryMedium(self):
  112. self._bridge.command(self.__name)(self.__mediumQueryReply)
  113. def __mediumQueryReply(self, slotName, path, flags = ''):
  114. print 'media query result of %s "%s" flags "%s"' % (
  115. slotName, path, flags
  116. )
  117. if slotName[-1] == ':':
  118. slotName = slotName[ : -1]
  119. else:
  120. print 'medium slot query reply does not start with "<medium>:", '\
  121. 'but with "%s"' % slotName
  122. return
  123. assert slotName == self.__name, 'medium slot reply not for ' \
  124. 'this slot? Got %s, expected %s.' % (slotName, self.__name)
  125. # TODO: Do something with the flags
  126. # TODO: Can we be sure that this reply was indeed for this (machine's)
  127. # slot?
  128. self.updateMedium(path)
  129. def updateMedium(self, path):
  130. if str(path) == '':
  131. medium = None
  132. else:
  133. medium = Medium.create(self.__name, path)
  134. self._medium = medium
  135. self.slotDataChanged.emit(self)
  136. def getName(self):
  137. return self.__name
  138. def setMedium(self, medium, errorHandler):
  139. if medium == self._medium:
  140. return False
  141. if medium is None:
  142. print 'ejecting from %s: %s' % (self.__name, self._medium)
  143. self._bridge.command(self.__name, 'eject')(
  144. None, errorHandler
  145. )
  146. else:
  147. optionList = self._createOptionList(medium)
  148. print 'insert into %s: %s (with options: %s)' % (self.__name,
  149. medium, str(optionList)
  150. )
  151. self._bridge.command(self.__name, 'insert',
  152. EscapedStr(tclEscape(medium.getPath())), *optionList)(
  153. None, lambda message, realHander = errorHandler: \
  154. self.__errorHandler(realHander, message)
  155. )
  156. self._medium = medium # no update sent, so we do it ourselves
  157. self.slotDataChanged.emit(self)
  158. def __errorHandler(self, realHandler, message):
  159. # call the actual errorhandler
  160. realHandler(message)
  161. # but also re-query openMSX for the actual situation
  162. self.__queryMedium()
  163. def _createOptionList(self, medium):
  164. assert medium is not None, 'We should never insert None'
  165. optionList = []
  166. return optionList
  167. def getMedium(self):
  168. return self._medium
  169. def __cmp__(self, other):
  170. # pylint: disable-msg=W0212
  171. return not isinstance(other, MediaSlot) \
  172. or cmp(self.__name, other.__name)
  173. def __str__(self):
  174. return 'MediaSlot with name %s and inserted medium %s' % (self.__name,
  175. self._medium or '<none>')
  176. class MediaSlotWithPatchableMedia(MediaSlot):
  177. def _createOptionList(self, medium):
  178. optionList = MediaSlot._createOptionList(self, medium)
  179. patchList = medium.getIpsPatchList()
  180. for option in patchList:
  181. optionList.append('-ips')
  182. optionList.append(option)
  183. return optionList
  184. class CassetteDeck(MediaSlot):
  185. stateChanged = Signal('QString')
  186. def __init__(self, name, bridge):
  187. MediaSlot.__init__(self, name, bridge)
  188. self.__state = ''
  189. self.__queryState()
  190. def __queryState(self):
  191. self._bridge.command('cassetteplayer')(self.__stateReply)
  192. def __stateReply(self, *words):
  193. self.setState(str(words[2]))
  194. def setMedium(self, medium, errorHandler):
  195. MediaSlot.setMedium(self, medium, errorHandler)
  196. if medium is not None:
  197. self._bridge.command('cassetteplayer', 'getlength')(
  198. self.__lengthReply, None)
  199. def __lengthReply(self, length):
  200. self._medium.setLength(length)
  201. self.slotDataChanged.emit(self)
  202. def getPosition(self, replyHandler, errorHandler):
  203. '''Query position of this medium to openMSX.
  204. It is not stored in this class, because there are no updates
  205. for it. This way, the user of this class can control the polling.
  206. '''
  207. self._bridge.command('cassetteplayer', 'getpos')(
  208. replyHandler, errorHandler)
  209. def setState(self, state):
  210. '''Only call this from MediaModel and from __stateReply and rewind!
  211. '''
  212. self.__state = state
  213. self.stateChanged.emit(state)
  214. def getState(self):
  215. assert self.__state != '', 'Illegal state!'
  216. return self.__state
  217. def rewind(self, errorHandler):
  218. self.setState('rewind') # rewind state is not sent by openMSX
  219. self._bridge.command('cassetteplayer', 'rewind')(
  220. lambda *dummy: self.__queryState(), errorHandler
  221. )
  222. def record(self, filename, errorHandler):
  223. self._bridge.command('cassetteplayer', 'new', filename)(
  224. None, errorHandler
  225. )
  226. def play(self, errorHandler):
  227. self._bridge.command('cassetteplayer', 'play')(
  228. None, errorHandler
  229. )
  230. class DiskDrive(MediaSlotWithPatchableMedia):
  231. pass
  232. class CartridgeSlot(MediaSlotWithPatchableMedia):
  233. def _createOptionList(self, medium):
  234. optionList = MediaSlotWithPatchableMedia._createOptionList(self, medium)
  235. assert isinstance(medium, CartridgeMedium), 'Wrong medium in cartridgeslot!'
  236. mapper = medium.getMapperType()
  237. if mapper != 'Auto Detect':
  238. optionList.append('-romtype')
  239. optionList.append(mapper)
  240. return optionList
  241. class MediaModel(QtCore.QAbstractListModel):
  242. dataChanged = QtSignal('QModelIndex', 'QModelIndex')
  243. mediaSlotRemoved = Signal('QString', 'QString')
  244. mediaSlotAdded = Signal('QString', 'QString')
  245. connected = Signal()
  246. def __init__(self, bridge, machineManager):
  247. QtCore.QAbstractListModel.__init__(self)
  248. self.__bridge = bridge
  249. self.__mediaSlotListForMachine = {} # keeps order, per machine
  250. # virtual_drive always exists and is machine independent
  251. self.__virtualDriveSlot = MediaSlot.create('virtual_drive', bridge)
  252. self.__romTypes = []
  253. self.__machineManager = machineManager
  254. bridge.registerInitial(self.__updateAll)
  255. bridge.registerUpdate('media', self.__updateMedium)
  256. bridge.registerUpdate('status', self.__updateCassetteDeckState)
  257. bridge.registerUpdatePrefix(
  258. 'hardware',
  259. ( 'cart', 'disk', 'cassette', 'hd', 'cd' ),
  260. self.__updateHardware
  261. )
  262. machineManager.machineAdded.connect(self.__machineAdded)
  263. machineManager.machineRemoved.connect(self.__machineRemoved)
  264. def __updateAll(self):
  265. # this is in the registerInitial callback, so:
  266. self.connected.emit()
  267. # TODO: The idea of the name "updateAll" was to be able to deal with
  268. # openMSX crashes. So, we should go back to knowing nothing about
  269. # the openMSX state.
  270. #self.__mediaSlotList = []
  271. for pattern in ( 'cart?', 'disk?', 'cassetteplayer', 'hd?',
  272. 'cd?'
  273. ):
  274. # Query medium slots.
  275. self.__bridge.command('info', 'command', pattern)(
  276. self.__mediumListReply
  277. )
  278. self.__bridge.command('openmsx_info', 'romtype')(self.__romTypeReply)
  279. def __romTypeReply(self, *romTypes):
  280. self.__romTypes.append('Auto Detect')
  281. for romType in romTypes:
  282. self.__romTypes.append(romType)
  283. def __mediumListReply(self, *slots):
  284. '''Callback to list the initial media slots of a particular type.
  285. '''
  286. if len(slots) == 0:
  287. return
  288. for mediumName in ( 'cart', 'disk', 'cassette', 'hd', 'cd' ):
  289. if slots[0].startswith(mediumName):
  290. break
  291. else:
  292. print 'media slot "%s" not recognised' % slots[0]
  293. return
  294. for slot in slots:
  295. self.__mediaSlotAdded(slot,
  296. # TODO: is this machineId still valid at this point in time?
  297. self.__machineManager.getCurrentMachineId()
  298. )
  299. def __machineAdded(self, machineId):
  300. print 'Adding media admin for machine with id ', machineId
  301. self.__mediaSlotListForMachine[unicode(machineId)] = []
  302. def __machineRemoved(self, machineId):
  303. print 'Removing media admin for machine with id ', machineId
  304. del self.__mediaSlotListForMachine[unicode(machineId)]
  305. def __mediaSlotAdded(self, slotName, machineId):
  306. slotList = self.__mediaSlotListForMachine[machineId]
  307. # add empty slot
  308. slot = MediaSlot.create(slotName, self.__bridge)
  309. index = bisect(slotList, slot)
  310. # we shouldn't have a slot with this name yet
  311. if slot in slotList:
  312. assert slot.getName() != slotName
  313. parent = QtCore.QModelIndex() # invalid model index
  314. self.beginInsertRows(parent, index, index)
  315. slotList.insert(index, slot)
  316. self.endInsertRows()
  317. self.mediaSlotAdded.emit(slotName, machineId)
  318. slot.slotDataChanged.connect(self.__slotDataChanged)
  319. def __mediaSlotRemoved(self, slotName, machineId):
  320. slotList = self.__mediaSlotListForMachine[machineId]
  321. for index, slot in enumerate(slotList):
  322. if slot.getName() == slotName:
  323. parent = QtCore.QModelIndex() # invalid model index
  324. print 'Removing "%s" for machine %s' % (slot, machineId)
  325. self.beginRemoveRows(parent, index, index)
  326. del slotList[index]
  327. self.endRemoveRows()
  328. slot.slotDataChanged.disconnect(self.__slotDataChanged)
  329. self.mediaSlotRemoved.emit(slotName, machineId)
  330. return
  331. assert False, 'removed slot "%s" did not exist' % slotName
  332. # this is for the updates coming from openMSX
  333. # forward to the slot
  334. def __updateMedium(self, mediaSlotName, machineId, path):
  335. slot = self.getMediaSlotByName(mediaSlotName, machineId)
  336. slot.updateMedium(path)
  337. def __slotDataChanged(self, slot):
  338. # virtual drive has no index
  339. if slot == self.__virtualDriveSlot:
  340. return
  341. # find this slot and emit a signal with its index
  342. for slotList in self.__mediaSlotListForMachine.itervalues():
  343. for index, iterSlot in enumerate(slotList):
  344. if slot is iterSlot:
  345. modelIndex = self.createIndex(index, 0)
  346. self.dataChanged.emit(modelIndex, modelIndex)
  347. return
  348. assert False, 'Slot not found: %s' % slot
  349. def __updateHardware(self, hardware, machineId, action):
  350. if action == 'add':
  351. self.__mediaSlotAdded(hardware, machineId)
  352. elif action == 'remove':
  353. self.__mediaSlotRemoved(hardware, machineId)
  354. else:
  355. print 'received update for unsupported action "%s" for ' \
  356. 'hardware "%s" on machine "%s".' \
  357. % ( action, hardware, machineId )
  358. def getMediaSlotByName(self, name, machineId = ''):
  359. '''Returns the media slot of the given machine, identified by the given
  360. name. Raises KeyError if no media slot exists by the given name.
  361. '''
  362. if name == 'virtual_drive':
  363. print 'Ignoring machineId "%s" for virtual_drive, ' \
  364. 'which is not machine bound...' % machineId
  365. return self.__virtualDriveSlot
  366. assert machineId != '', 'You need to pass a machineId!'
  367. slotList = self.__mediaSlotListForMachine[machineId]
  368. for slot in slotList:
  369. if slot.getName() == name:
  370. return slot
  371. raise KeyError(name)
  372. def rowCount(self, parent):
  373. # TODO: What does this mean?
  374. if parent.isValid():
  375. return 0
  376. else:
  377. machineId = self.__machineManager.getCurrentMachineId()
  378. try:
  379. count = len(self.__mediaSlotListForMachine[machineId])
  380. except KeyError:
  381. # can happen when switching machines or when the
  382. # current machine is not known yet
  383. count = 0
  384. return count
  385. def data(self, index, role = QtCore.Qt.DisplayRole):
  386. if not index.isValid():
  387. return QtCore.QVariant()
  388. machineId = self.__machineManager.getCurrentMachineId()
  389. slotList = self.__mediaSlotListForMachine[machineId]
  390. row = index.row()
  391. if row < 0 or row > (len(slotList) - 1):
  392. # can happen when switching machines (race conditions?)
  393. # print '*********************************************************'
  394. return QtCore.QVariant()
  395. slot = slotList[row]
  396. slotName = slot.getName()
  397. if role == QtCore.Qt.DisplayRole:
  398. if slotName.startswith('cart'):
  399. description = 'Cartridge slot %s' % slotName[-1].upper()
  400. elif slotName.startswith('disk'):
  401. description = 'Disk drive %s' % slotName[-1].upper()
  402. elif slotName.startswith('cassette'):
  403. description = 'Cassette deck'
  404. elif slotName.startswith('hd'):
  405. description = 'Hard disk drive %s' % slotName[-1].upper()
  406. elif slotName.startswith('cd'):
  407. description = 'CD-ROM drive %s' % slotName[-1].upper()
  408. # elif slotName.startswith('virtual'):
  409. # # Don't display anything for this entry!!
  410. # return QtCore.QVariant()
  411. else:
  412. description = slotName.upper()
  413. medium = slot.getMedium()
  414. if medium:
  415. path = medium.getPath()
  416. dirName, fileName = os.path.split(path)
  417. if fileName == '':
  418. fileName = dirName[dirName.rfind(os.path.sep) + 1 : ]
  419. else:
  420. fileName = '<empty>'
  421. return QtCore.QVariant(
  422. '%s: %s' % ( description, fileName )
  423. )
  424. elif role == QtCore.Qt.UserRole:
  425. return QtCore.QVariant(slotName)
  426. return QtCore.QVariant()
  427. def iterDriveNames(self):
  428. machineId = self.__machineManager.getCurrentMachineId()
  429. slotList = self.__mediaSlotListForMachine[machineId][:]
  430. slotList.append(self.__virtualDriveSlot)
  431. for slot in slotList:
  432. name = slot.getName()
  433. if name.startswith('disk') or name.startswith('hd') \
  434. or name == 'virtual_drive':
  435. yield name
  436. def getRomTypes(self):
  437. return self.__romTypes
  438. # forward cassette deck state update to the proper slot
  439. def __updateCassetteDeckState(self, name, machineId, state):
  440. name = str(name)
  441. if name == 'cassetteplayer':
  442. print 'State of cassetteplayer updated to ', state
  443. deck = self.getMediaSlotByName(name, machineId)
  444. deck.setState(state)