From 347af27784cca6310f62ad7dc65a070405a75878 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Tue, 22 Mar 2016 23:01:55 +0100 Subject: [PATCH] rework the new screen DB stuff: - 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 | 74 +++++++++++++++++++++-------------------- qt_frontend.py | 42 +++++++++++++++-------- question_frontend.py | 12 +++---- screen.py | 79 +++++++++++++++++--------------------------- 4 files changed, 104 insertions(+), 103 deletions(-) diff --git a/lilass b/lilass index 5dfd19a..b9283eb 100755 --- 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) diff --git a/qt_frontend.py b/qt_frontend.py index b3ee61e..0c86555 100644 --- a/qt_frontend.py +++ b/qt_frontend.py @@ -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) diff --git a/question_frontend.py b/question_frontend.py index fc9e8c6..4244ba8 100644 --- a/question_frontend.py +++ b/question_frontend.py @@ -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) diff --git a/screen.py b/screen.py index 6ba9418..a95d45d 100644 --- 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 """""" % (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 , 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: -- 2.30.2