Index: src/mlx/xplane.py
===================================================================
--- src/mlx/xplane.py	(revision 421)
+++ src/mlx/xplane.py	(revision 423)
@@ -15,4 +15,5 @@
 from xplra import TYPE_INT, TYPE_FLOAT, TYPE_DOUBLE
 from xplra import TYPE_FLOAT_ARRAY, TYPE_INT_ARRAY, TYPE_BYTE_ARRAY
+from xplra import HOTKEY_MODIFIER_SHIFT, HOTKEY_MODIFIER_CONTROL
 
 #------------------------------------------------------------------------------
@@ -30,4 +31,198 @@
 
 _mps2knots = 3600.0 / 1852
+
+#------------------------------------------------------------------------------
+
+class Request(object):
+    """Base class for one-shot requests."""
+    def __init__(self, handler, callback, extra):
+        """Construct the request."""
+        self._handler = handler
+        self._callback = callback
+        self._extra = extra
+        self._result = None
+
+    def process(self, time):
+        """Process the request.
+
+        Return True if the request has succeeded, False if data validation
+        has failed for a reading request. An exception may also be thrown
+        if there is some lower-level communication problem."""
+        if self._process(time):
+            Handler._callSafe(lambda: self._callback(self._result,
+                                                     self._extra))
+            return True
+        else:
+            return False
+
+    def fail(self):
+        """Handle the failure of this request."""
+        Handler._callSafe(lambda: self._callback(False, self._extra))
+
+class DataRequest(Request):
+    """A simple, one-shot data read or write request."""
+    def __init__(self, handler, forWrite, data, callback, extra,
+                 validator = None):
+        """Construct the request."""
+        super(DataRequest, self).__init__(handler, callback, extra)
+
+        self._forWrite = forWrite
+        self._validator = validator
+
+        xplane = handler._xplane
+        self._multiBuffer = xplane.createMultiSetter() if forWrite \
+                            else xplane.createMultiGetter()
+
+        Handler._setupMultiBuffer(self._multiBuffer,
+                                  [(d[0], d[1]) for d in data])
+
+        if forWrite:
+            index = 0
+            for (_, _, value) in data:
+                self._multiBuffer[index] = value
+                index += 1
+
+
+    def fail(self):
+        """Handle the failure of this request."""
+        if self._forWrite:
+            super(DataRequest, self).fail()
+        else:
+            Handler._callSafe(lambda: self._callback(None, self._extra))
+
+    def _process(self, time):
+        """Process the request."""
+        if self._forWrite:
+            self._multiBuffer.execute()
+            self._result = True
+            return True
+        elif Handler._performRead(self._multiBuffer,
+                                  self._extra, self._validator):
+            self._result = self._multiBuffer
+            return True
+        else:
+            return False
+
+class ShowMessageRequest(Request):
+    """Request to show a message in the simulator window."""
+    def __init__(self, handler, message, duration, callback, extra):
+        """Construct the request."""
+        super(ShowMessageRequest, self).__init__(handler,
+                                                         callback, extra)
+        self._message = message
+        self._duration = duration
+
+    def _process(self, time):
+        """Process the request."""
+        self._handler._xplane.showMessage(self._message, self._duration)
+        self._result = True
+        return True
+
+class RegisterHotkeysRequest(Request):
+    """Request to register hotkeys with the simulator."""
+    def __init__(self, handler, hotkeyCodes, callback, extra):
+        """Construct the request."""
+        super(RegisterHotkeysRequest, self).__init__(handler,
+                                                     callback,
+                                                     extra)
+        self._hotkeyCodes = hotkeyCodes
+
+    def _process(self, time):
+        """Process the request."""
+        self._handler._xplane.registerHotkeys(self._hotkeyCodes)
+        self._result = True
+        return True
+
+class UnregisterHotkeysRequest(Request):
+    """Request to register hotkeys with the simulator."""
+    def _process(self, time):
+        """Process the request."""
+        self._handler._xplane.unregisterHotkeys()
+        self._result = True
+        return True
+
+class PeriodicRequest(object):
+    """A periodic request."""
+    def __init__(self, handler, id, period, callback, extra):
+        """Construct the periodic request."""
+        self._handler = handler
+        self._id = id
+        self._period = period
+        self._nextFire = time.time()
+        self._callback = callback
+        self._extra = extra
+        self._result = None
+
+    @property
+    def id(self):
+        """Get the ID of this periodic request."""
+        return self._id
+
+    @property
+    def nextFire(self):
+        """Get the next firing time."""
+        return self._nextFire
+
+    def process(self, now):
+        """Check if this request should be executed, and if so, do so.
+
+        now is the time at which the request is being executed. If this
+        function is called too early, nothing is done, and True is
+        returned.
+
+        Return True if the request has succeeded, False if data validation
+        has failed. An exception may also be thrown if there is some
+        lower-level communication problem."""
+        if now<self._nextFire:
+            return True
+
+        isOK = self._process(time)
+
+        if isOK:
+            Handler._callSafe(lambda: self._callback(self._result,
+                                                     self._extra))
+            now = time.time()
+            while self._nextFire <= now:
+                self._nextFire += self._period
+
+        return isOK
+
+    def fail(self):
+        """Handle the failure of this request."""
+        pass
+
+    def __cmp__(self, other):
+        """Compare two periodic requests. They are ordered by their next
+        firing times."""
+        return cmp(self._nextFire, other._nextFire)
+
+class PeriodicDataRequest(PeriodicRequest):
+    """A periodic request."""
+    def __init__(self, handler, id, period, data, callback, extra,
+                 validator):
+        """Construct the periodic request."""
+        super(PeriodicDataRequest, self).__init__(handler, id, period,
+                                                  callback, extra)
+        self._validator = validator
+        self._multiGetter = handler._xplane.createMultiGetter()
+        Handler._setupMultiBuffer(self._multiGetter, data)
+
+    def _process(self, now):
+        """Process the request."""
+        if Handler._performRead(self._multiGetter,
+                                self._extra, self._validator):
+            self._result = self._multiGetter
+            return True
+        else:
+            return False
+
+#------------------------------------------------------------------------------
+
+class HotkeysStateRequest(PeriodicRequest):
+    """Periodic hotkey query request."""
+    def _process(self, now):
+        """Process the request."""
+        self._result = self._handler._xplane.queryHotkeys()
+        return True
 
 #------------------------------------------------------------------------------
@@ -97,5 +292,5 @@
 
     @staticmethod
-    def _performRead(multiGetter, callback, extra, validator):
+    def _performRead(multiGetter, extra, validator):
         """Perform a read request.
 
@@ -112,5 +307,4 @@
             if validator is None or \
                Handler._callSafe(lambda: validator(multiGetter, extra)):
-                Handler._callSafe(lambda: callback(multiGetter, extra))
                 return True
             else:
@@ -118,127 +312,4 @@
         return False
 
-    class Request(object):
-        """A simple, one-shot request."""
-        def __init__(self, handler, forWrite, data, callback, extra,
-                     validator = None):
-            """Construct the request."""
-            self._forWrite = forWrite
-            self._callback = callback
-            self._extra = extra
-            self._validator = validator
-
-            xplane = handler._xplane
-            self._multiBuffer = xplane.createMultiSetter() if forWrite \
-                                else xplane.createMultiGetter()
-
-            Handler._setupMultiBuffer(self._multiBuffer,
-                                      [(d[0], d[1]) for d in data])
-
-            if forWrite:
-                index = 0
-                for (_, _, value) in data:
-                    self._multiBuffer[index] = value
-                    index += 1
-
-        def process(self, time):
-            """Process the request.
-
-            Return True if the request has succeeded, False if data validation
-            has failed for a reading request. An exception may also be thrown
-            if there is some lower-level communication problem."""
-            if self._forWrite:
-                self._multiBuffer.execute()
-                Handler._callSafe(lambda: self._callback(True, self._extra))
-                return True
-            else:
-                return Handler._performRead(self._multiBuffer, self._callback,
-                                            self._extra, self._validator)
-
-        def fail(self):
-            """Handle the failure of this request."""
-            if self._forWrite:
-                Handler._callSafe(lambda: self._callback(False, self._extra))
-            else:
-                Handler._callSafe(lambda: self._callback(None, self._extra))
-
-    class PeriodicRequest(object):
-        """A periodic request."""
-        def __init__(self, handler, id, period, data, callback, extra,
-                     validator):
-            """Construct the periodic request."""
-            self._id = id
-            self._period = period
-            self._nextFire = time.time()
-            self._callback = callback
-            self._extra = extra
-            self._validator = validator
-
-            self._multiGetter = handler._xplane.createMultiGetter()
-            Handler._setupMultiBuffer(self._multiGetter, data)
-
-        @property
-        def id(self):
-            """Get the ID of this periodic request."""
-            return self._id
-
-        @property
-        def nextFire(self):
-            """Get the next firing time."""
-            return self._nextFire
-
-        def process(self, time):
-            """Check if this request should be executed, and if so, do so.
-
-            time is the time at which the request is being executed. If this
-            function is called too early, nothing is done, and True is
-            returned.
-
-            Return True if the request has succeeded, False if data validation
-            has failed. An exception may also be thrown if there is some
-            lower-level communication problem."""
-            if time<self._nextFire:
-                return True
-
-            isOK = Handler._performRead(self._multiGetter, self._callback,
-                                        self._extra, self._validator)
-
-            if isOK:
-                while self._nextFire <= time:
-                    self._nextFire += self._period
-
-            return isOK
-
-        def fail(self):
-            """Handle the failure of this request."""
-            pass
-
-        def __cmp__(self, other):
-            """Compare two periodic requests. They are ordered by their next
-            firing times."""
-            return cmp(self._nextFire, other._nextFire)
-
-    class ShowMessageRequest(object):
-        """Request to show a message in the simulator window."""
-        def __init__(self, handler, message, duration, callback, extra):
-            """Construct the request."""
-            self._handler = handler
-            self._message = message
-            self._duration = duration
-            self._callback = callback
-            self._extra = extra
-
-        def process(self, time):
-            """Process the request.
-
-            Return True if the request has succeeded. An exception may also be
-            thrown if there is some lower-level communication problem."""
-            self._handler._xplane.showMessage(self._message, self._duration)
-            Handler._callSafe(lambda: self._callback(True, self._extra))
-            return True
-
-        def fail(self):
-            """Handle the failure of this request."""
-            Handler._callSafe(lambda: self._callback(False, self._extra))
-
     def __init__(self, connectionListener,
                  connectAttempts = -1, connectInterval = 0.2):
@@ -276,7 +347,7 @@
         """
         with self._requestCondition:
-            self._requests.append(Handler.Request(self, False, data,
-                                                  callback, extra,
-                                                  validator))
+            self._requests.append(DataRequest(self, False, data,
+                                              callback, extra,
+                                              validator))
             self._requestCondition.notify()
 
@@ -295,5 +366,5 @@
         """
         with self._requestCondition:
-            request = Handler.Request(self, True, data, callback, extra)
+            request = DataRequest(self, True, data, callback, extra)
             #print "xplane.Handler.requestWrite", request
             self._requests.append(request)
@@ -311,6 +382,7 @@
             id = self._nextPeriodicID
             self._nextPeriodicID += 1
-            request = Handler.PeriodicRequest(self, id, period, data, callback,
-                                              extra, validator)
+            request = PeriodicDataRequest(self, id, period,
+                                          data, callback,
+                                          extra, validator)
             self._periodicRequests.append(request)
             self._requestCondition.notify()
@@ -329,7 +401,31 @@
         """Request showing a message in the simulator."""
         with self._requestCondition:
-            self._requests.append(Handler.ShowMessageRequest(self,
-                                                             message, duration,
-                                                             callback, extra))
+            self._requests.append(ShowMessageRequest(self,
+                                                     message, duration,
+                                                     callback, extra))
+            self._requestCondition.notify()
+
+    def registerHotkeys(self, hotkeys, callback, extra = None):
+        """Request registering the given hotkeys."""
+        with self._requestCondition:
+            self._requests.append(RegisterHotkeysRequest(self, hotkeys,
+                                                         callback, extra))
+            self._requestCondition.notify()
+
+    def requestHotkeysState(self, period, callback, extra = None):
+        """Request a periodic query of the hotkey status."""
+        with self._requestCondition:
+            id = self._nextPeriodicID
+            self._nextPeriodicID += 1
+            request = HotkeysStateRequest(self, id, period, callback, extra)
+            self._periodicRequests.append(request)
+            self._requestCondition.notify()
+            return id
+
+    def unregisterHotkeys(self, callback, extra = None):
+        """Request unregistering the hotkeys."""
+        with self._requestCondition:
+            self._requests.append(UnregisterHotkeysRequest(self,
+                                                           callback, extra))
             self._requestCondition.notify()
 
@@ -557,17 +653,10 @@
 
     @staticmethod
-    def _appendHotkeyData(data, offset, hotkey):
-        """Append the data for the given hotkey to the given array, that is
-        intended to be passed to requestWrite call on the handler."""
-        data.append((offset + 0, "b", ord(hotkey.key)))
-
-        modifiers = 0
-        if hotkey.ctrl: modifiers |= 0x02
-        if hotkey.shift: modifiers |= 0x01
-        data.append((offset + 1, "b", modifiers))
-
-        data.append((offset + 2, "b", 0))
-
-        data.append((offset + 3, "b", 0))
+    def _getHotkeyCode(hotkey):
+        """Get the hotkey code for the given hot key."""
+        code = ord(hotkey.key)
+        if hotkey.shift: code |= HOTKEY_MODIFIER_SHIFT
+        if hotkey.ctrl: code |= HOTKEY_MODIFIER_CONTROL
+        return code
 
     def __init__(self, connectionListener, connectAttempts = -1,
@@ -619,5 +708,5 @@
 
         self._hotkeyLock = threading.Lock()
-        self._hotkeys = None
+        self._hotkeyCodes = None
         self._hotkeySetID = 0
         self._hotkeySetGeneration = 0
@@ -749,20 +838,19 @@
         - the ID of the hotkey set as returned by this function,
         - the list of the indexes of the hotkeys that were pressed."""
-        pass
-        # TODO: implement hotkey handling for X-Plane
-        # with self._hotkeyLock:
-        #     assert self._hotkeys is None
-
-        #     self._hotkeys = hotkeys
-        #     self._hotkeySetID += 1
-        #     self._hotkeySetGeneration = 0
-        #     self._hotkeyCallback = callback
-
-        #     self._handler.requestRead([(0x320c, "u")],
-        #                               self._handleNumHotkeys,
-        #                               (self._hotkeySetID,
-        #                                self._hotkeySetGeneration))
-
-        #     return self._hotkeySetID
+        with self._hotkeyLock:
+            assert self._hotkeyCodes is None
+
+            self._hotkeyCodes = \
+              [self._getHotkeyCode(hotkey) for hotkey in hotkeys]
+            self._hotkeySetID += 1
+            self._hotkeySetGeneration = 0
+            self._hotkeyCallback = callback
+
+            self._handler.registerHotkeys(self._hotkeyCodes,
+                                          self._handleHotkeysRegistered,
+                                          (self._hotkeySetID,
+                                           self._hotkeySetGeneration))
+
+            return self._hotkeySetID
 
     def clearHotkeys(self):
@@ -777,11 +865,10 @@
         clearHotkeys(), this stored ID should be cleared so that the check
         fails for sure."""
-        pass
-        # with self._hotkeyLock:
-        #     if self._hotkeys is not None:
-        #         self._hotkeys = None
-        #         self._hotkeySetID += 1
-        #         self._hotkeyCallback = None
-        #         self._clearHotkeyRequest()
+        with self._hotkeyLock:
+            if self._hotkeyCodes is not None:
+                self._hotkeyCodes = None
+                self._hotkeySetID += 1
+                self._hotkeyCallback = None
+                self._clearHotkeyRequest()
 
     def disconnect(self, closingMessage = None, duration = 3):
@@ -803,13 +890,13 @@
         simulator of the given type."""
         self._fsType = fsType
-        # TODO: implement hotkey handling for X-Plane
-        # with self._hotkeyLock:
-        #     if self._hotkeys is not None:
-        #         self._hotkeySetGeneration += 1
-
-        #         self._handler.requestRead([(0x320c, "u")],
-        #                                   self._handleNumHotkeys,
-        #                                   (self._hotkeySetID,
-        #                                    self._hotkeySetGeneration))
+        with self._hotkeyLock:
+            if self._hotkeyCodes is not None:
+                self._hotkeySetGeneration += 1
+
+                self._handler.registerHotkeys(self._hotkeyCodes,
+                                              self._handleHotkeysRegistered,
+                                              (self._hotkeySetID,
+                                               self._hotkeySetGeneration))
+
         self._connectionListener.connected(fsType, descriptor)
 
@@ -1024,78 +1111,13 @@
             self._handler.disconnect()
 
-    def _handleNumHotkeys(self, data, (id, generation)):
-        """Handle the result of the query of the number of hotkeys"""
-        pass
-        # TODO: implement hotkey handling for X-Plane
-        # with self._hotkeyLock:
-        #     if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
-        #         numHotkeys = data[0]
-        #         print "xplra.Simulator._handleNumHotkeys: numHotkeys:", numHotkeys
-        #         data = [(0x3210 + i*4, "d") for i in range(0, numHotkeys)]
-        #         self._handler.requestRead(data, self._handleHotkeyTable,
-        #                                   (id, generation))
-
-    def _setupHotkeys(self, data):
-        """Setup the hiven hotkeys and return the data to be written.
-
-        If there were hotkeys set previously, they are reused as much as
-        possible. Any of them not reused will be cleared."""
-        # TODO: implement hotkey handling for X-Plane
-        hotkeys = self._hotkeys
-        numHotkeys = len(hotkeys)
-
-        oldHotkeyOffsets = set([] if self._hotkeyOffets is None else
-                               self._hotkeyOffets)
-
-        self._hotkeyOffets = []
-        numOffsets = 0
-
-        while oldHotkeyOffsets:
-            offset = oldHotkeyOffsets.pop()
-            self._hotkeyOffets.append(offset)
-            numOffsets += 1
-
-            if numOffsets>=numHotkeys:
-                break
-
-        for i in range(0, len(data)):
-            if numOffsets>=numHotkeys:
-                break
-
-            if data[i]==0:
-                self._hotkeyOffets.append(0x3210 + i*4)
-                numOffsets += 1
-
-        writeData = []
-        for i in range(0, numOffsets):
-            Simulator._appendHotkeyData(writeData,
-                                        self._hotkeyOffets[i],
-                                        hotkeys[i])
-
-        for offset in oldHotkeyOffsets:
-            writeData.append((offset, "u", long(0)))
-
-        return writeData
-
-    def _handleHotkeyTable(self, data, (id, generation)):
-        """Handle the result of the query of the hotkey table."""
-        with self._hotkeyLock:
-            if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
-                writeData = self._setupHotkeys(data)
-                self._handler.requestWrite(writeData,
-                                           self._handleHotkeysWritten,
-                                           (id, generation))
-
-    def _handleHotkeysWritten(self, success, (id, generation)):
+    def _handleHotkeysRegistered(self, success, (id, generation)):
         """Handle the result of the hotkeys having been written."""
         with self._hotkeyLock:
             if success and id==self._hotkeySetID and \
             generation==self._hotkeySetGeneration:
-                data = [(offset + 3, "b") for offset in self._hotkeyOffets]
-
                 self._hotkeyRequestID = \
-                    self._handler.requestPeriodicRead(0.5, data,
-                                                      self._handleHotkeys,
-                                                      (id, generation))
+                  self._handler.requestHotkeysState(0.5,
+                                                    self._handleHotkeys,
+                                                    (id, generation))
 
     def _handleHotkeys(self, data, (id, generation)):
@@ -1114,19 +1136,16 @@
 
         if hotkeysPressed:
-            data = []
-            for index in hotkeysPressed:
-                data.append((offsets[index]+3, "b", int(0)))
-            self._handler.requestWrite(data, self._handleHotkeysCleared)
-
             callback(id, hotkeysPressed)
-
-    def _handleHotkeysCleared(self, sucess, extra):
-        """Callback for the hotkey-clearing write request."""
 
     def _clearHotkeyRequest(self):
         """Clear the hotkey request in the handler if there is any."""
         if self._hotkeyRequestID is not None:
+            self._handler.unregisterHotkeys(self._hotkeysUnregistered)
             self._handler.clearPeriodic(self._hotkeyRequestID)
             self._hotkeyRequestID = None
+
+    def _hotkeysUnregistered(self):
+        """Called when the hotkeys have been unregistered."""
+        pass
 
 #------------------------------------------------------------------------------
