Revert "made aspect ratio accurate"
[lilass.git] / screen.py
index a543fea49b195b4dbe0769672c84a228a8dbae50..6ba9418ae39058429b092aa4e1a646d91582e489 100644 (file)
--- a/screen.py
+++ b/screen.py
@@ -36,12 +36,18 @@ def processOutputIt(*args):
 
 class RelativeScreenPosition(Enum):
     '''Represents the relative position of the external screen to the internal one'''
 
 class RelativeScreenPosition(Enum):
     '''Represents the relative position of the external screen to the internal one'''
-    LEFT      = 0
-    RIGHT     = 1
-    ABOVE     = 2
-    BELOW     = 3
-    MIRROR    = 4
-
+    LEFT      = ("left of")
+    RIGHT     = ("right of")
+    ABOVE     = ("above")
+    BELOW     = ("below")
+    MIRROR    = ("same as")
+    def __init__(self, text):
+        # auto numbering
+        cls = self.__class__
+        self._value_ = len(cls.__members__) + 1
+        self.text = text
+    def __str__(self):
+        return self.text
 
 class Resolution:
     '''Represents a resolution of a screen'''
 
 class Resolution:
     '''Represents a resolution of a screen'''
@@ -49,6 +55,24 @@ class Resolution:
         self.width = width
         self.height = height
     
         self.width = width
         self.height = height
     
+    @classmethod
+    def fromDatabase(cls, dbstr):
+        if dbstr is None:
+            return None
+        parts = dbstr.split("x")
+        if len(parts) != 2:
+            raise ValueError(xrandrstr)
+        return Resolution(*map(int,parts))
+    
+    def forDatabase(self):
+        return str(self.width)+'x'+str(self.height)
+    
+    def forXrandr(self):
+        return self.forDatabase()
+    
+    def toTuple(self):
+        return (self.width, self.height)
+    
     def __eq__(self, other):
         if not isinstance(other, Resolution):
             return False
     def __eq__(self, other):
         if not isinstance(other, Resolution):
             return False
@@ -57,6 +81,9 @@ class Resolution:
     def __ne__(self, other):
         return not self.__eq__(other)
     
     def __ne__(self, other):
         return not self.__eq__(other)
     
+    def __hash__(self):
+        return hash(("Resolution",self.width,self.height))
+    
     def __str__(self):
         # get ratio
         ratio = int(round(16.0*self.height/self.width))
     def __str__(self):
         # get ratio
         ratio = int(round(16.0*self.height/self.width))
@@ -71,8 +98,8 @@ class Resolution:
     def __repr__(self):
         return 'screen.Resolution('+self.forXrandr()+')'
     
     def __repr__(self):
         return 'screen.Resolution('+self.forXrandr()+')'
     
-    def forXrandr(self):
-        return str(self.width)+'x'+str(self.height)
+    def pixelCount(self):
+        return self.width * self.height
 
 
 class ScreenSetup:
 
 
 class ScreenSetup:
@@ -113,12 +140,64 @@ class ScreenSetup:
                 RelativeScreenPosition.MIRROR: '--same-as',
             }[self.relPosition], intName]
         return args
                 RelativeScreenPosition.MIRROR: '--same-as',
             }[self.relPosition], intName]
         return args
+    
+    def __str__(self):
+        if self.intResolution is None:
+            return "External display only, at "+str(self.extResolution)
+        if self.extResolution is None:
+            return "Internal display only, at "+str(self.intResolution)
+        return "External display %s at %s %s internal display %s at %s" % ("(primary)" if self.extIsPrimary else "", str(self.extResolution), str(self.relPosition), "" if self.extIsPrimary else "(primary)", str(self.intResolution))
 
 
+class Connector:
+    def __init__(self, name=None):
+        self.name = name # connector name, e.g. "HDMI1"
+        self.edid = None # EDID string for the connector, or None if disconnected
+        self._resolutions = set() # list of Resolution objects, empty if disconnected
+        self.preferredResolution = None
+        self.__lastResolution = None
+        self.hasLastResolution = False
+    
+    def __str__(self):
+        return str(self.name)
+    
+    def __repr__(self):
+        return """<Connector "%s" EDID="%s" resolutions="%s">""" % (str(self.name), str(self.edid), ", ".join(str(r) for r in self.getResolutionList()))
+    
+    def __setLastRes(self, res):
+        # res == None means this display was last switched off
+        if res is not None and not res in self._resolutions:
+            raise ValueError("Resolution "+res+" not available for "+self.name+".")
+        self.__lastResolution = res
+        self.hasLastResolution = True
+    
+    def __getLastRes(self):
+        if not self.hasLastResolution:
+            raise ValueError("Connector %s has no last known resolution." % self.name)
+        return self.__lastResolution
+    
+    lastResolution = property(__getLastRes, __setLastRes)
+    
+    def isConnected(self):
+        assert (self.edid is None) == (len(self._resolutions)==0)
+        return self.edid is not None
+    
+    def addResolution(self, resolution):
+        assert isinstance(resolution, Resolution)
+        self._resolutions.add(resolution)
+    
+    def appendToEdid(self, s):
+        if self.edid is None:
+            self.edid = s
+        else:
+            self.edid += s
+    
+    def getResolutionList(self):
+        return sorted(self._resolutions, key=lambda r: (0 if self.hasLastResolution and r==self.lastResolution else 1, 0 if r==self.preferredResolution else 1, -r.pixelCount()))
 
 class ScreenSituation:
 
 class ScreenSituation:
-    connectors = {} # maps connector names to lists of Resolution (empty list -> disabled connector)
-    internalConnector = None # name of the internal connector (will be an enabled one)
-    externalConnector = None # name of the used external connector (an enabled one), or None
+    connectors = [] # contains all the Connector objects
+    internalConnector = None # the internal Connector object (will be an enabled one)
+    externalConnector = None # the used external Connector object (an enabled one), or None
     
     '''Represents the "screen situation" a machine can be in: Which connectors exist, which resolutions do they have, what are the names for the internal and external screen'''
     def __init__(self, internalConnectorNames, externalConnectorNames = None):
     
     '''Represents the "screen situation" a machine can be in: Which connectors exist, which resolutions do they have, what are the names for the internal and external screen'''
     def __init__(self, internalConnectorNames, externalConnectorNames = None):
@@ -126,23 +205,37 @@ class ScreenSituation:
            just choose any remaining connector.'''
         # which connectors are there?
         self._getXrandrInformation()
            just choose any remaining connector.'''
         # which connectors are there?
         self._getXrandrInformation()
-        print(self.connectors)
+        for c in self.connectors:
+            print(repr(c))
+            print()
         # figure out which is the internal connector
         self.internalConnector = self._findAvailableConnector(internalConnectorNames)
         if self.internalConnector is None:
         # figure out which is the internal connector
         self.internalConnector = self._findAvailableConnector(internalConnectorNames)
         if self.internalConnector is None:
-            raise Exception("Could not automatically find internal connector, please use ~/.dsl.conf to specify it manually.")
-        print(self.internalConnector)
+            raise Exception("Could not automatically find internal connector, please use (or fix) ~/.dsl.conf to specify it manually.")
+        print("Detected internal connector:",self.internalConnector)
         # and the external one
         if externalConnectorNames is None:
         # and the external one
         if externalConnectorNames is None:
-            externalConnectorNames = list(self.connectors.keys())
-            externalConnectorNames.remove(self.internalConnector)
+            externalConnectorNames = map(lambda c: c.name, self.connectors)
+            externalConnectorNames = set(filter(lambda name: name != self.internalConnector.name, externalConnectorNames))
         self.externalConnector = self._findAvailableConnector(externalConnectorNames)
         self.externalConnector = self._findAvailableConnector(externalConnectorNames)
-        print(self.externalConnector)
+        if self.internalConnector == self.externalConnector:
+            raise Exception("Internal and external connector are the same. This must not happen. Please fix ~/.dsl.conf.");
+        print("Detected external connector:",self.externalConnector)
+        # self.lastSetup is left uninitialized so you can't access it before trying a lookup in the database
     
     # Run xrandr and fill the dict of connector names mapped to lists of available resolutions.
     def _getXrandrInformation(self):
         connector = None # current connector
     
     # Run xrandr and fill the dict of connector names mapped to lists of available resolutions.
     def _getXrandrInformation(self):
         connector = None # current connector
-        for line in processOutputGen("xrandr", "-q"):
+        readingEdid = False
+        for line in processOutputGen("xrandr", "-q", "--verbose"):
+            if readingEdid:
+                m = re.match(r'^\s*([0-9a-f]+)\s*$', line)
+                if m is not None:
+                    connector.appendToEdid(m.group(1))
+                    continue
+                else:
+                    readingEdid = False
+                    # fallthrough to the rest of the loop for parsing of this line
             # screen?
             m = re.search(r'^Screen [0-9]+: ', line)
             if m is not None: # ignore this line
             # screen?
             m = re.search(r'^Screen [0-9]+: ', line)
             if m is not None: # ignore this line
@@ -151,55 +244,61 @@ class ScreenSituation:
             # new connector?
             m = re.search(r'^([\w\-]+) (dis)?connected ', line)
             if m is not None:
             # new connector?
             m = re.search(r'^([\w\-]+) (dis)?connected ', line)
             if m is not None:
-                connector = m.groups()[0]
-                assert connector not in self.connectors
-                self.connectors[connector] = []
+                connector = Connector(m.group(1))
+                assert not any(c.name == connector.name for c in self.connectors)
+                self.connectors.append(connector)
                 continue
             # new resolution?
                 continue
             # new resolution?
-            m = re.search(r'^   ([\d]+)x([\d]+) +', line)
+            m = re.search(r'^\s*([\d]+)x([\d]+)', line)
             if m is not None:
             if m is not None:
-                resolution = Resolution(int(m.groups()[0]), int(m.groups()[1]))
+                resolution = Resolution(int(m.group(1)), int(m.group(2)))
                 assert connector is not None
                 assert connector is not None
-                self.connectors[connector].append(resolution)
+                connector.addResolution(resolution)
+                if '+preferred' in line:
+                    connector.preferredResolution = resolution
+                continue
+            # EDID?
+            m = re.search(r'^\s*EDID:\s*$', line)
+            if m is not None:
+                readingEdid = True
                 continue
             # unknown line
             # not fatal, e.g. xrandr shows strange stuff when a display is enabled, but not connected
                 continue
             # unknown line
             # not fatal, e.g. xrandr shows strange stuff when a display is enabled, but not connected
-            print("Warning: Unknown xrandr line %s" % line)
+            #print("Warning: Unknown xrandr line %s" % line)
     
     
-    # return the first available connector from those listed in <tryConnectors>, skipping disabled connectors
-    def _findAvailableConnector(self, tryConnectors):
-        for connector in tryConnectors:
-            if connector in self.connectors and len(self.connectors[connector]): # if the connector exists and is active (i.e. there is a resolution)
-                return connector
+    # return the first available connector from those listed in <tryConnectorNames>, skipping disabled connectors
+    def _findAvailableConnector(self, tryConnectorNames):
+        for c in filter(lambda c: c.name in tryConnectorNames and c.isConnected(), self.connectors):
+            return c
         return None
     
     # return available internal resolutions
     def internalResolutions(self):
         return None
     
     # return available internal resolutions
     def internalResolutions(self):
-        return self.connectors[self.internalConnector]
+        return self.internalConnector.getResolutionList()
     
     # return available external resolutions (or None, if there is no external screen connected)
     def externalResolutions(self):
         if self.externalConnector is None:
             return None
     
     # return available external resolutions (or None, if there is no external screen connected)
     def externalResolutions(self):
         if self.externalConnector is None:
             return None
-        return self.connectors[self.externalConnector]
+        return self.externalConnector.getResolutionList()
     
     # return resolutions available for both internal and external screen
     def commonResolutions(self):
         internalRes = self.internalResolutions()
         externalRes = self.externalResolutions()
         assert externalRes is not None
     
     # return resolutions available for both internal and external screen
     def commonResolutions(self):
         internalRes = self.internalResolutions()
         externalRes = self.externalResolutions()
         assert externalRes is not None
-        return [res for res in externalRes if res in internalRes]
+        return sorted(set(externalRes).intersection(internalRes), key=lambda r: -r.pixelCount())
     
     # compute the xrandr call
     def forXrandr(self, setup):
         # turn all screens off
         connectorArgs = {} # maps connector names to xrand arguments
     
     # compute the xrandr call
     def forXrandr(self, setup):
         # turn all screens off
         connectorArgs = {} # maps connector names to xrand arguments
-        for c in self.connectors.keys():
-            connectorArgs[c] = ["--off"]
+        for c in self.connectors:
+            connectorArgs[c.name] = ["--off"]
         # set arguments for the relevant ones
         # set arguments for the relevant ones
-        connectorArgs[self.internalConnector] = setup.getInternalArgs()
+        connectorArgs[self.internalConnector.name] = setup.getInternalArgs()
         if self.externalConnector is not None:
         if self.externalConnector is not None:
-            connectorArgs[self.externalConnector] = setup.getExternalArgs(self.internalConnector)
+            connectorArgs[self.externalConnector.name] = setup.getExternalArgs(self.internalConnector.name)
         else:
             assert setup.extResolution is None, "There's no external screen to set a resolution for"
         # now compose the arguments
         else:
             assert setup.extResolution is None, "There's no external screen to set a resolution for"
         # now compose the arguments
@@ -208,3 +307,19 @@ class ScreenSituation:
             call += ["--output", name] + connectorArgs[name]
         return call
 
             call += ["--output", name] + connectorArgs[name]
         return call
 
+    def fetchDBInfo(self, db):
+        if self.externalConnector and self.externalConnector.edid:
+            self.lastSetup = db.getConfig(self.externalConnector.edid) # may also return None
+        else:
+            self.lastSetup = None
+        if self.lastSetup:
+            print("SETUP FOUND", self.lastSetup)
+            self.externalConnector.lastResolution = self.lastSetup.extResolution
+            self.internalConnector.lastResolution = self.lastSetup.intResolution
+        else:
+            print("NO SETUP FOUND")
+    
+    def putDBInfo(self, db, setup):
+        if not self.externalConnector or not self.externalConnector.edid:
+            return
+        db.putConfig(self.externalConnector.edid, setup)