rework the new screen DB stuff:
authorRalf Jung <post@ralfj.de>
Tue, 22 Mar 2016 22:01:55 +0000 (23:01 +0100)
committerRalf Jung <post@ralfj.de>
Tue, 22 Mar 2016 22:03:32 +0000 (23:03 +0100)
- The tri-state --show actually did not make that much sense, which was particualrily obvious because of questions like
  "What should the default mode be?". We have -r/-e/-i for that. So replace --show by --silent, such that e.g.,
  `lilass -s -r mirror` will use the saved data and fall back to mirror mode, never showing a UI under normal circumstances.
  On the other hand, `lilass -s` *will* show a UI for unknown screens only.
- Rename last* -> previous*
- Sort resolutions by size only (not depending on preferred/previous resolution)
- Qt UI: pre-select previous/preferred resolution
- Remove some Debug output

lilass
qt_frontend.py
question_frontend.py
screen.py

diff --git a/lilass b/lilass
index 5dfd19a70cd21d0bb7a38400ecef4dcb65c421a8..b9283ebe93f6897fa47bfde9d1b1076ef1cc769e 100755 (executable)
--- a/lilass
+++ b/lilass
@@ -20,7 +20,7 @@ import argparse, sys, os, os.path, shutil, re, subprocess
 from enum import Enum
 import gui, screen, util, database
 frontend = gui.getFrontend("cli") # the fallback, until we got a proper frontend. This is guaranteed to be available.
-
+cmdArgs = None
 
 # for auto-config: common names of internal connectors
 commonInternalConnectorPrefixes = ['LVDS', 'eDP']
@@ -104,16 +104,19 @@ if __name__ == "__main__":
                             help="The frontend to be used for user interaction")
         parser.add_argument("-r", "--relative-position",
                             dest="rel_position", choices=list(map(relPosFilter, screen.RelativeScreenPosition.__members__.keys())),
-                            help="Set the position of external screen relative to internal one.")
+                            help="Set the position of external screen relative to internal one, in case it is not found in the DB.")
         parser.add_argument("-e", "--external-only",
                             dest="external_only", action='store_true',
                             help="If an external screen is connected, disable all the others.")
         parser.add_argument("-i", "--internal-only",
                             dest="internal_only", action='store_true',
                             help="Enable internal screen, disable all the others.")
-        parser.add_argument("-s", "--show",
-                            dest="show", choices=ShowLevels.getNames(), default=ShowLevels.ONEXTERNAL.text,
-                            help="In which situations should the UI be displayed?")
+        parser.add_argument("-s", "--silent",
+                            dest="silent", action='store_true',
+                            help="Prefer to be silent: Opens a UI only if the external screen is not known *and* no default configuration (-r/-e/-i) is given.")
+        parser.add_argument("-v", "--verbose",
+                            dest="verbose", action='store_true',
+                            help="More verbose output on stderr.")
         cmdArgs = parser.parse_args()
     
         # load frontend early (for error mssages)
@@ -139,17 +142,29 @@ if __name__ == "__main__":
         # see what situation we are in
         situation = situationByConfig(config)
         
-        # fetch info from the database
-        # if it is too slow to open the DB twice (reading and saving), we can keep it open all the time
-        with database.Database(databaseFilePath) as db:
-            situation.fetchDBInfo(db)
-        
         # construct the ScreenSetup
         setup = None
-        if not cmdArgs.internal_only and situation.externalResolutions() is not None:
-            # there's an external screen connected that we may want to use
-            if cmdArgs.external_only:
-                setup = screen.ScreenSetup(intResolution = None, extResolution = situation.externalResolutions()[0])
+        if situation.externalConnector is not None:
+            # There's an external screen connected that we may want to use.
+            # Fetch info about this screen from the database.
+            # NOTE: If it is too slow to open the DB twice (reading and saving), we can keep it open all the time
+            with database.Database(databaseFilePath) as db:
+                situation.fetchDBInfo(db)
+            # what to we do?
+            have_default_conf = bool(cmdArgs.external_only or cmdArgs.internal_only or cmdArgs.rel_position)
+            no_ui = bool(have_default_conf or (situation.previousSetup and cmdArgs.silent))
+            if not no_ui:
+                # ask the user what to do
+                setup = frontend.setup(situation)
+                if setup is None: sys.exit(1) # the user canceled
+                with database.Database(databaseFilePath) as db:
+                    situation.putDBInfo(db, setup)
+            elif situation.previousSetup:
+                # apply the old setup again
+                setup = situation.previousSetup
+            # use default config from CLI
+            elif cmdArgs.external_only:
+                setup = screen.ScreenSetup(intResolution = None, extResolution = situation.externalConnector.getPreferredResolution())
             elif cmdArgs.rel_position is not None:
                 # construct automatically, based on CLI arguments
                 # first, figure out the desired RelativeScreenPosition... waht a bad hack...
@@ -161,26 +176,14 @@ if __name__ == "__main__":
                     res = situation.commonResolutions()[0]
                     setup = screen.ScreenSetup(res, res, relPos)
                 else:
-                    setup = screen.ScreenSetup(intResolution = situation.internalResolutions()[0], extResolution = situation.externalResolutions()[0], relPosition = relPos)
-            else:
-                showlvl = ShowLevels(cmdArgs.show)
-                if showlvl != ShowLevels.ONEXTERNAL and situation.lastSetup:
-                    # use last config
-                    setup = situation.lastSetup
-                elif showlvl == ShowLevels.ONERROR:
-                    # guess config
-                    setup = screen.ScreenSetup(situation.internalResolutions()[0], situation.externalResolutions()[0], screen.RelativeScreenPosition.RIGHT)
-                    # TODO make default relative position configurable in the config file
-                    # TODO this has a bit of code duplication with the cmdArgs method above
-                else:
-                    # ask the user
-                    setup = frontend.setup(situation)
-                    if setup is None: sys.exit(1) # the user canceled
-                    with database.Database(databaseFilePath) as db:
-                        situation.putDBInfo(db, setup)
-        else:
-            # use first resolution of internal connector
-            setup = screen.ScreenSetup(intResolution = situation.internalConnector.getResolutionList()[0], extResolution = None)
+                    setup = screen.ScreenSetup(intResolution = situation.internalConnector.getPreferredResolution(),
+                                               extResolution = situation.externalConnector.getPreferredResolution(),
+                                               relPosition = relPos)
+            # cmdArgs.internal_only: fall-through
+        if setup is None:
+            assert cmdArgs.internal_only or situation.externalConnector is None
+            # Nothing chosen yet? Use first resolution of internal connector.
+            setup = screen.ScreenSetup(intResolution = situation.internalConnector.getPreferredResolution(), extResolution = None)
         
         # call xrandr
         xrandrCall = situation.forXrandr(setup)
@@ -191,5 +194,6 @@ if __name__ == "__main__":
         if setup.extResolution is None:
             turnOnBacklight()
     except Exception as e:
-        raise e
         frontend.error(str(e))
+        if cmdArgs is None or cmdArgs.verbose:
+            raise(e)
index b3ee61e41a5afc6ac3e052985fd2bc7c8dd73c94..0c86555127b86aef9e6906a4c9690373653d9351 100644 (file)
@@ -42,19 +42,31 @@ try:
             syncIfMirror(self.intRes, self.extRes)
             syncIfMirror(self.extRes, self.intRes)
 
+            # if situation has a previousSetup, use its values as initial state
+            if situation.previousSetup:
+                p = situation.previousSetup
+                self.intEnabled.setChecked(p.intResolution is not None)
+                self.extEnabled.setChecked(p.extResolution is not None)
+                if p.relPosition:
+                    self.relPos.setCurrentIndex(p.relPosition.value - 1)
+                if p.extIsPrimary:
+                    self.extPrimary.setChecked(True)
+                else:
+                    self.intPrimary.setChecked(True)
+                # Pre-select the previous resolution
+                self._intDefaultRes = p.intResolution
+                self._extDefaultRes = p.extResolution
+                self._mirrorDefaultRes = p.intResolution if p.relPosition == RelativeScreenPosition.MIRROR else None # in case of a mirror, they would be the same anyway
+            else:
+                self._intDefaultRes = situation.internalConnector.getPreferredResolution()
+                self._extDefaultRes = situation.externalConnector.getPreferredResolution()
+                self._mirrorDefaultRes = None
+
             # connect the update function
             self.intEnabled.toggled.connect(self.updateEnabledControls)
             self.extEnabled.toggled.connect(self.updateEnabledControls)
             self.relPos.currentIndexChanged.connect(self.updateEnabledControls)
 
-            # if situation has a lastSetup, use its values as initial state
-            if situation.lastSetup:
-                last = situation.lastSetup
-                self.intEnabled.setChecked(last.intResolution is not None)
-                self.extEnabled.setChecked(last.extResolution is not None)
-                if last.relPosition:
-                    self.relPos.setCurrentIndex(last.relPosition.value-1)
-
             # make sure we are in a correct state
             self.updateEnabledControls()
 
@@ -62,12 +74,14 @@ try:
             idx = self.relPos.currentIndex()
             return self.relPos.itemData(idx)
         
-        def fillResolutionBox(self, box, resolutions):
+        def fillResolutionBox(self, box, resolutions, select = None):
             # if the count did not change, update in-place (this avoids flicker)
             if box.count() == len(resolutions):
                 for idx, res in enumerate(resolutions):
                     box.setItemText(idx, str(res))
                     box.setItemData(idx, res)
+                    if res == select:
+                        box.setCurrentIndex(idx)
             else:
                 # first clear it
                 while box.count() > 0:
@@ -75,6 +89,8 @@ try:
                 # then fill it
                 for res in resolutions:
                     box.addItem(str(res), res)
+                    if res == select:
+                        box.setCurrentIndex(box.count() - 1) # select the most recently added one
         
         def updateEnabledControls(self):
             intEnabled = self.intEnabled.isChecked()
@@ -93,12 +109,12 @@ try:
             # which resolutions do we offer?
             if self.isMirror:
                 commonRes = self._situation.commonResolutions()
-                self.fillResolutionBox(self.intRes, commonRes)
-                self.fillResolutionBox(self.extRes, commonRes)
+                self.fillResolutionBox(self.intRes, commonRes, select = self._mirrorDefaultRes)
+                self.fillResolutionBox(self.extRes, commonRes, select = self._mirrorDefaultRes)
                 self.intRes.setCurrentIndex(self.extRes.currentIndex())
             else:
-                self.fillResolutionBox(self.intRes, self._situation.internalResolutions())
-                self.fillResolutionBox(self.extRes, self._situation.externalResolutions())
+                self.fillResolutionBox(self.intRes, self._situation.internalConnector.getResolutionList(), select = self._intDefaultRes)
+                self.fillResolutionBox(self.extRes, self._situation.externalConnector.getResolutionList(), select = self._extDefaultRes)
             # configure position control
             self.posGroup.setEnabled(bothEnabled)
             self.posLabel1.setEnabled(bothEnabled)
index fc9e8c6dd0048f7c585f500d671451843c24e26e..4244ba8d1234763f1f128ecae7bb9b9810e8f980 100644 (file)
@@ -40,13 +40,13 @@ class QuestionFrontend:
         return self.userChoose("Select resolution for %s"%displayname, modedescs, availablemodes, None)
 
     def setup (self, situation):
-        if situation.lastSetup:
-            applyLast = self.userChoose("This display is known. The last setup for it was like this:\n%s.\nApply the last used configuration?" % str(situation.lastSetup), ("Apply last setup", "Enter different setup"), (True,False), None)
-            if applyLast is None:
+        if situation.previousSetup:
+            applyPrevious = self.userChoose("This display is known. The last setup for it was like this:\n%s.\nApply the last used configuration?" % str(situation.previousSetup), ("Apply last setup", "Enter different setup"), (True,False), None)
+            if applyPrevious is None:
                 return None
-            if applyLast is True:
-                return situation.lastSetup
-            assert applyLast is False
+            if applyPrevious is True:
+                return situation.previousSetup
+            assert applyPrevious is False
         operationmodes = list(OperationMode)
         operationmodedescs = list(map(lambda x: x.text, operationmodes))
         operationmode = self.userChoose ("Display setup", operationmodedescs, operationmodes, None)
index 6ba9418ae39058429b092aa4e1a646d91582e489..a95d45dc8d19cd4f8a39715baa2332e323899786 100644 (file)
--- a/screen.py
+++ b/screen.py
@@ -152,9 +152,9 @@ 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._resolutions = set() # set of Resolution objects, empty if disconnected
+        self._preferredResolution = None
+        self.previousResolution = None
         self.hasLastResolution = False
     
     def __str__(self):
@@ -163,22 +163,8 @@ class Connector:
     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)
+        assert (self.edid is None) == (len(self._resolutions)==0), "Resolution-EDID mismatch; #resolutions: {}".format(len(self._resolutions))
         return self.edid is not None
     
     def addResolution(self, resolution):
@@ -191,13 +177,23 @@ class Connector:
         else:
             self.edid += s
     
+    def setPreferredResolution(self, resolution):
+        assert isinstance(resolution, Resolution) and resolution in self._resolutions
+        self._preferredResolution = resolution
+    
+    def getPreferredResolution(self):
+        if self._preferredResolution is not None:
+            return self._preferredResolution
+        return self.getResolutionList()[0] # prefer the largest resolution
+    
     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()))
+        return sorted(self._resolutions, key=lambda r: -r.pixelCount())
 
 class ScreenSituation:
     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
+    previousSetup = None # None or the ScreenSetup used the last time this external screen was connected
     
     '''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):
@@ -205,9 +201,6 @@ class ScreenSituation:
            just choose any remaining connector.'''
         # which connectors are there?
         self._getXrandrInformation()
-        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:
@@ -221,7 +214,6 @@ class ScreenSituation:
         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):
@@ -246,7 +238,9 @@ class ScreenSituation:
             if m is not None:
                 connector = Connector(m.group(1))
                 assert not any(c.name == connector.name for c in self.connectors)
-                self.connectors.append(connector)
+                if not connector.name.startswith("VIRTUAL"):
+                    # skip "VIRTUAL" connectors
+                    self.connectors.append(connector)
                 continue
             # new resolution?
             m = re.search(r'^\s*([\d]+)x([\d]+)', line)
@@ -254,8 +248,8 @@ class ScreenSituation:
                 resolution = Resolution(int(m.group(1)), int(m.group(2)))
                 assert connector is not None
                 connector.addResolution(resolution)
-                if '+preferred' in line:
-                    connector.preferredResolution = resolution
+                if re.search(r' [+]preferred\b', line):
+                    connector.setPreferredResolution(resolution)
                 continue
             # EDID?
             m = re.search(r'^\s*EDID:\s*$', line)
@@ -263,8 +257,7 @@ class ScreenSituation:
                 readingEdid = True
                 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)
+            # not fatal, e.g. xrandr shows strange stuff when a display is enabled, but not connected; --verbose adds a whole lot of other weird stuff
     
     # return the first available connector from those listed in <tryConnectorNames>, skipping disabled connectors
     def _findAvailableConnector(self, tryConnectorNames):
@@ -272,21 +265,11 @@ class ScreenSituation:
             return c
         return None
     
-    # return available internal resolutions
-    def internalResolutions(self):
-        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 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
+        assert self.externalConnector is not None, "Common resolutions may only be queried if there is an external screen connected."
+        internalRes = self.internalConnector.getResolutionList()
+        externalRes = self.externalConnector.getResolutionList()
         return sorted(set(externalRes).intersection(internalRes), key=lambda r: -r.pixelCount())
     
     # compute the xrandr call
@@ -309,15 +292,13 @@ class ScreenSituation:
 
     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
+            self.previousSetup = db.getConfig(self.externalConnector.edid) # may also return None
         else:
-            print("NO SETUP FOUND")
+            self.previousSetup = None
+        if self.previousSetup:
+            print("Known screen, previous setup:", self.previousSetup)
+            self.externalConnector.previousResolution = self.previousSetup.extResolution
+            self.internalConnector.previousResolution = self.previousSetup.intResolution
     
     def putDBInfo(self, db, setup):
         if not self.externalConnector or not self.externalConnector.edid: