refactor pretty much everything. some features are still missing.
authorRalf Jung <post@ralfj.de>
Sun, 8 Feb 2015 11:55:05 +0000 (12:55 +0100)
committerRalf Jung <post@ralfj.de>
Sun, 8 Feb 2015 11:55:27 +0000 (12:55 +0100)
dsl.py
gui.py
qt_dialogue.py
qt_dialogue.ui [new file with mode: 0644]
screen.py [new file with mode: 0644]
zenity_dialogue.py

diff --git a/dsl.py b/dsl.py
index 707a5c008cf2d85172879d289046cc29d5563550..41db08c6539abe11a1debcfd3c276749f6d81338 100755 (executable)
--- a/dsl.py
+++ b/dsl.py
@@ -1,6 +1,6 @@
 #!/usr/bin/python3
 # DSL - easy Display Setup for Laptops
 #!/usr/bin/python3
 # DSL - easy Display Setup for Laptops
-# Copyright (C) 2012-2014 Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 
 import argparse, sys, os, re, subprocess
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 
 import argparse, sys, os, re, subprocess
-from gui import getFrontend
-frontend = getFrontend("cli") # the fallback, until we got a proper frontend. This is guaranteed to be available.
+from enum import Enum
+import gui, screen
+frontend = gui.getFrontend("cli") # the fallback, until we got a proper frontend. This is guaranteed to be available.
 
 # for auto-config: common names of internal connectors
 commonInternalConnectorPrefixes = ['LVDS', 'eDP']
 commonInternalConnectorSuffices = ['', '0', '1', '-0', '-1']
 
 # for auto-config: common names of internal connectors
 commonInternalConnectorPrefixes = ['LVDS', 'eDP']
 commonInternalConnectorSuffices = ['', '0', '1', '-0', '-1']
+def commonInternalConnectorNames():
+    for prefix in commonInternalConnectorPrefixes:
+        for suffix in commonInternalConnectorSuffices:
+            yield prefix+suffix
 
 
-# this is as close as one can get to an enum in Python
-class RelativeScreenPosition:
-    LEFT          = 0
-    RIGHT         = 1
-    EXTERNAL_ONLY = 2
-    MIRROR        = 3
-    
-    __names__ = {
-        'left': LEFT,
-        'right': RIGHT,
-        'external-only': EXTERNAL_ONLY,
-        'mirror': MIRROR
-    }
-
-# storing what's necessary for screen setup
-class ScreenSetup:
-    def __init__(self, relPosition, intResolution, extResolution, extIsPrimary = False):
-        '''relPosition must be one of the RelativeScreenPosition members, the resolutions must be (width, height) pairs'''
-        self.relPosition = relPosition
-        self.intResolution = intResolution # value doesn't matter if the internal screen is disabled
-        self.extResolution = extResolution
-        self.extIsPrimary = extIsPrimary or self.relPosition == RelativeScreenPosition.EXTERNAL_ONLY # external is always primary if it is the only one
-    
-    def getInternalArgs(self):
-        if self.relPosition == RelativeScreenPosition.EXTERNAL_ONLY:
-            return ["--off"]
-        args = ["--mode", res2xrandr(self.intResolution)] # set internal screen to desired resolution
-        if not self.extIsPrimary:
-            args.append('--primary')
-        return args
-    
-    def getExternalArgs(self, intName):
-        args = ["--mode", res2xrandr(self.extResolution)] # set external screen to desired resolution
-        if self.extIsPrimary:
-            args.append('--primary')
-        # set position
-        if self.relPosition == RelativeScreenPosition.LEFT:
-            args += ['--left-of', intName]
-        elif self.relPosition == RelativeScreenPosition.RIGHT:
-            args += ['--right-of', intName]
-        elif self.relPosition == RelativeScreenPosition.MIRROR:
-            args += ['--same-as', intName]
-        else:
-            assert self.relPosition == RelativeScreenPosition.EXTERNAL_ONLY
-        return args
 
 # Load a section-less config file: maps parameter names to space-separated lists of strings (with shell quotation)
 def loadConfigFile(filename):
 
 # Load a section-less config file: maps parameter names to space-separated lists of strings (with shell quotation)
 def loadConfigFile(filename):
@@ -93,109 +53,18 @@ def loadConfigFile(filename):
     # add some convencience get functions
     return result
 
     # add some convencience get functions
     return result
 
-# iterator yielding common names of internal connectors
-def commonInternalConnectorNames():
-    for prefix in commonInternalConnectorPrefixes:
-        for suffix in commonInternalConnectorSuffices:
-            yield prefix+suffix
-
-# helper function: execute a process, return output as iterator, throw exception if there was an error
-# you *must* iterate to the end if you use this!
-def processOutputGen(*args):
-    with subprocess.Popen(args, stdout=subprocess.PIPE) as p:
-        for line in p.stdout:
-            yield line.decode("utf-8")
-    if p.returncode != 0:
-        raise Exception("Error executing "+str(args))
-def processOutputIt(*args):
-    return list(processOutputGen(*args)) # list() iterates over the generator
-
-# Run xrandr and return a dict of output names mapped to lists of available resolutions, each being a (width, height) pair.
-# An empty list indicates that the connector is disabled.
-def getXrandrInformation():
-    connectors = {} # map of connector names to a list of resolutions
-    connector = None # current connector
-    for line in processOutputGen("xrandr", "-q"):
-        # screen?
-        m = re.search(r'^Screen [0-9]+: ', line)
-        if m is not None: # ignore this line
-            connector = None
-            continue
-        # new connector?
-        m = re.search(r'^([\w\-]+) (dis)?connected ', line)
-        if m is not None:
-            connector = m.groups()[0]
-            assert connector not in connectors
-            connectors[connector] = []
-            continue
-        # new resolution?
-        m = re.search(r'^   ([\d]+)x([\d]+) +', line)
-        if m is not None:
-            assert connector is not None
-            connectors[connector].append((int(m.groups()[0]), int(m.groups()[1])))
-            continue
-        # unknown line
-        # not fatal as my xrandr shows strange stuff when a display is enabled, but not connected
-        #raise Exception("Unknown line in xrandr output:\n"+line)
-        print("Warning: Unknown xrandr line %s" % line)
-    return connectors
 
 
-# convert a (width, height) pair into a string accepted by xrandr as argument for --mode
-def res2xrandr(res):
-    (w, h) = res
-    return str(w)+'x'+str(h)
-
-# convert a (width, height) pair into a string to be displayed to the user
-def res2user(res):
-    (w, h) = res
-    # get ratio
-    ratio = int(round(16.0*h/w))
-    if ratio == 12: # 16:12 = 4:3
-        strRatio = '4:3'
-    elif ratio == 13: # 16:12.8 = 5:4
-        strRatio = '5:4'
-    else: # let's just hope this will never be 14 or more...
-        strRatio = '16:%d' % ratio
-    return '%dx%d (%s)' %(w, h, strRatio)
-
-# return the first available connector from those listed in tryConnectors, skipping disabled connectors
-def findAvailableConnector(tryConnectors, allConnectors):
-    for connector in tryConnectors:
-        if connector in allConnectors and allConnectors[connector]: # if the connector exists and is active (i.e. there is a resolution)
-            return connector
-    return None
+# Make sure the backlight is turned on
+def turnOnBacklight():
+    try:
+        backlight = float(subprocess.check_output(["xbacklight", "-get"]).strip())
+        if backlight == 0: # it's completely turned off, we better enable it
+            subprocess.check_call(["xbacklight", "-set", "100"])
+    except FileNotFoundError:
+        print("xbacklight has not been found, unable to turn your laptop backlight on.")
+    except subprocess.CalledProcessError:
+        print("xbacklight returned an error while attempting to turn your laptop backlight on.")
 
 
-# Return a (internalConnector, externalConnectors) pair: The name of the internal connector, and a list of external connectors.
-# Use the config file at ~/.dsl.conf and fall back to auto-detection
-def classifyConnectors(allConnectors):
-    config = loadConfigFile(os.getenv('HOME') + '/.dsl.conf')
-    # find internal connector
-    if 'internalConnector' in config:
-        if len(config['internalConnector']) != 1:
-            raise Exception("You must specify exactly one internal connector.")
-        internalConnector = config['internalConnector'][0]
-        if not internalConnector in allConnectors:
-            raise Exception("Connector %s does not exist, there is an error in your config file." % internalConnector)
-    else:
-        # auto-config
-        internalConnector = findAvailableConnector(commonInternalConnectorNames(), allConnectors)
-        if internalConnector is None:
-            raise Exception("Could not automatically find internal connector, please use ~/.dsl.conf to specify it manually.")
-    # all the rest is external then, obviously - unless the user wants to do that manually
-    if 'externalConnectors' in config:
-        externalConnectors = config['externalConnectors']
-        for connector in externalConnectors:
-            if not connector in allConnectors:
-                raise Exception("Connector %s does not exist, there is an error in your config file." % connector)
-            if connector == internalConnector:
-                raise Exception("%s is both internal and external, that doesn't make sense." % connector)
-    else:
-        externalConnectors = list(allConnectors.keys())
-        externalConnectors.remove(internalConnector)
-    if not externalConnectors:
-        raise Exception("No external connector found - either your config is wrong, or your machine has only one connector.")
-    # done!
-    return (internalConnector, externalConnectors)
 
 # if we run top-level
 if __name__ == "__main__":
 
 # if we run top-level
 if __name__ == "__main__":
@@ -205,67 +74,36 @@ if __name__ == "__main__":
                         dest="frontend",
                         help="The frontend to be used for user interaction")
     parser.add_argument("-r", "--relative-position",
                         dest="frontend",
                         help="The frontend to be used for user interaction")
     parser.add_argument("-r", "--relative-position",
-                        dest="rel_position", choices=RelativeScreenPosition.__names__.keys(),
+                        dest="rel_position", choices=list(map(str.lower, screen.RelativeScreenPosition.__members__.keys())),
                         help="Position of external screen relative to internal one")
     parser.add_argument("-i", "--internal-only",
                         dest="internal_only", action='store_true',
                         help="Enable internal screen, disable all the others (as if no external screen was connected")
     cmdArgs = parser.parse_args()
     
                         help="Position of external screen relative to internal one")
     parser.add_argument("-i", "--internal-only",
                         dest="internal_only", action='store_true',
                         help="Enable internal screen, disable all the others (as if no external screen was connected")
     cmdArgs = parser.parse_args()
     
-    # load frontend
-    frontend = getFrontend(cmdArgs.frontend)
+    # load frontend early (for error mssages)
+    frontend = gui.getFrontend(cmdArgs.frontend)
     try:
     try:
-        # load connectors and classify them
-        connectors = getXrandrInformation()
-        (internalConnector, externalConnectors) = classifyConnectors(connectors)
+        # see what situation we are in
+        situation = screen.ScreenSituation(commonInternalConnectorNames())
         
         
-        # default: screen off
-        connectorArgs = {} # maps connector names to xrand arguments
-        for c in externalConnectors+[internalConnector]:
-            connectorArgs[c] = ["--off"]
-        
-        # check whether we got an external screen or not
-        usedExternalConnector = findAvailableConnector(externalConnectors, connectors) # *the* external connector which is actually used
-        hasExternal = not cmdArgs.internal_only and usedExternalConnector is not None
-        if hasExternal:
-            # compute the list of resolutons available on both
-            commonRes = [res for res in connectors[usedExternalConnector] if res in connectors[internalConnector]]
-            # there's an external screen connected, we need to get a setup
-            if cmdArgs.rel_position is not None:
-                # use command-line arguments
-                relPosition = RelativeScreenPosition.__names__[cmdArgs.rel_position]
-                if relPosition == RelativeScreenPosition.MIRROR:
-                    setup = ScreenSetup(relPosition, commonRes[0], commonRes[0]) # use default resolutions
-                else:
-                    setup = ScreenSetup(relPosition, connectors[internalConnector][0], connectors[usedExternalConnector][0]) # use default resolutions
-            else:
-                # use GUI
-                setup = frontend.setup(connectors[internalConnector], connectors[usedExternalConnector], commonRes)
+        # construct the ScreenSetup
+        setup = None
+        if situation.externalResolutions() is not None:
+            setup = frontend.setup(situation)
             if setup is None: sys.exit(1) # the user canceled
             if setup is None: sys.exit(1) # the user canceled
-            # apply it
-            connectorArgs[internalConnector] = setup.getInternalArgs()
-            connectorArgs[usedExternalConnector] = setup.getExternalArgs(internalConnector)
         else:
             # use first resolution of internal connector
         else:
             # use first resolution of internal connector
-            connectorArgs[internalConnector] = ["--mode", res2xrandr(connectors[internalConnector][0]), "--primary"]
+            setup = ScreenSetup(intResolution = situation.internalResolutions()[0], extResolution = None)
         
         
-        # and do it
-        call = ["xrandr"]
-        for name in connectorArgs:
-            call += ["--output", name] + connectorArgs[name]
-        print("Call that will be made:",call)
-        subprocess.check_call(call)
+        # call xrandr
+        xrandrCall = situation.forXrandr(setup)
+        print("Call that will be made:",xrandrCall)
+        subprocess.check_call(xrandrCall)
         
         # make sure the internal screen is really, *really* turned on if there is no external screen
         
         # make sure the internal screen is really, *really* turned on if there is no external screen
-        if not hasExternal:
-            try:
-                backlight = float(subprocess.check_output(["xbacklight", "-get"]).strip())
-                if backlight == 0: # it's completely turned off, we better enable it
-                    subprocess.check_call(["xbacklight", "-set", "100"])
-            except FileNotFoundError:
-                print("xbacklight has not been found, unable to turn your laptop backlight on.")
-            except subprocess.CalledProcessError:
-                print("xbacklight returned an error while attempting to turn your laptop backlight on.")
+        if setup.extResolution is None:
+            turnOnBacklight()
     except Exception as e:
         frontend.error(str(e))
         raise
     except Exception as e:
         frontend.error(str(e))
         raise
diff --git a/gui.py b/gui.py
index 40cafa55e8dc2cda6c0850cbfa42723f407b8c99..c1f045d6c1d5ecdeae46f46307c821aaa73f9386 100644 (file)
--- a/gui.py
+++ b/gui.py
@@ -1,5 +1,5 @@
 # DSL - easy Display Setup for Laptops
 # DSL - easy Display Setup for Laptops
-# Copyright (C) 2012 Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -42,9 +42,9 @@ class QtFrontend:
         from PyQt4 import QtGui
         QtGui.QMessageBox.critical(None, 'Fatal error', message)
     
         from PyQt4 import QtGui
         QtGui.QMessageBox.critical(None, 'Fatal error', message)
     
-    def setup(self, internalResolutions, externalResolutions, commonRes):
+    def setup(self, situation):
         from qt_dialogue import PositionSelection
         from qt_dialogue import PositionSelection
-        return PositionSelection(internalResolutions, externalResolutions, commonRes).run()
+        return PositionSelection(situation).run()
     
     @staticmethod
     def isAvailable():
     
     @staticmethod
     def isAvailable():
@@ -61,9 +61,9 @@ class ZenityFrontend:
         '''Displays a fatal error to the user'''
         subprocess.check_call(["zenity", "--error", "--text="+message])
     
         '''Displays a fatal error to the user'''
         subprocess.check_call(["zenity", "--error", "--text="+message])
     
-    def setup(self, internalResolutions, externalResolutions, commonRes):
+    def setup(self, situation):
         from zenity_dialogue import run
         from zenity_dialogue import run
-        return run(internalResolutions, externalResolutions)
+        return run(situation.internalResolutions(), situation.externalResolutions())
     
     @staticmethod
     def isAvailable():
     
     @staticmethod
     def isAvailable():
index c82b8f9ee6f543aaa5c89dbf0776d55bf8203fea..96c04a73e74c6e3f9a6e2205d5b1c7a47a8da727 100644 (file)
@@ -1,5 +1,5 @@
 # DSL - easy Display Setup for Laptops
 # DSL - easy Display Setup for Laptops
-# Copyright (C) 2012 Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-from dsl import RelativeScreenPosition, ScreenSetup, res2user
-from PyQt4 import QtCore, QtGui
+import os
+from screen import RelativeScreenPosition, ScreenSetup
+from PyQt4 import QtCore, QtGui, uic
+
+relPosNames = {
+    RelativeScreenPosition.LEFT: "left of",
+    RelativeScreenPosition.RIGHT: "right of",
+    RelativeScreenPosition.ABOVE: "above",
+    RelativeScreenPosition.BELOW: "below",
+    RelativeScreenPosition.MIRROR: "same as",
+}
 
 
-def makeLayout(layout, members):
-    for m in members:
-        if isinstance(m, QtGui.QLayout):
-            layout.addLayout(m)
-        else:
-            layout.addWidget(m)
-    return layout
 
 class PositionSelection(QtGui.QDialog):
 
 class PositionSelection(QtGui.QDialog):
-    def __init__(self, internalResolutions, externalResolutions, commonResolutions):
+    def __init__(self, situation):
         # set up main window
         super(PositionSelection, self).__init__()
         # set up main window
         super(PositionSelection, self).__init__()
-        self.setWindowTitle('DSL - easy Display Setup for Laptops')
-        
-        ## position selection
-        posBox = QtGui.QGroupBox('Position of external screen', self)
-        self.posLeft = QtGui.QRadioButton('Left of internal screen', posBox)
-        self.posRight = QtGui.QRadioButton('Right of internal screen', posBox)
-        self.posRight.setChecked(True)
-        self.posRight.setFocus()
-        self.extOnly = QtGui.QRadioButton('Use external screen exclusively', posBox)
-        self.mirror = QtGui.QRadioButton('Mirror internal screen', posBox)
-        positions = [self.posLeft, self.posRight, self.extOnly, self.mirror]
-        posBox.setLayout(makeLayout(QtGui.QVBoxLayout(), positions))
-        for pos in positions:
-            pos.toggled.connect(self.updateForm)
-        
-        ## primary screen
-        self.primBox = QtGui.QGroupBox('Which should be the primary screen?', self)
-        self.primExt = QtGui.QRadioButton('The external screen', self.primBox)
-        self.primInt = QtGui.QRadioButton('The internal screen', self.primBox)
-        self.primInt.setChecked(True)
-        self.primBox.setLayout(makeLayout(QtGui.QVBoxLayout(), [self.primExt, self.primInt]))
+        self._situation = situation
+        uifile = os.path.join(os.path.dirname(__file__), 'qt_dialogue.ui')
+        uic.loadUi(uifile, self)
         
         
-        ## resolution selection
-        resBox = QtGui.QGroupBox('Screen resolutions', self)
-        # external screen
-        self.extResLabel = QtGui.QLabel('Resolution of external screen:', resBox)
-        self.extResolutions = externalResolutions
-        self.extResolutionsBox = QtGui.QComboBox(resBox)
-        for res in externalResolutions:
-            self.extResolutionsBox.addItem(res2user(res))
-        self.extResolutionsBox.setCurrentIndex(0) # select first resolution
-        self.extRow = makeLayout(QtGui.QHBoxLayout(), [self.extResLabel, self.extResolutionsBox])
-        # internal screen
-        self.intResLabel = QtGui.QLabel('Resolution of internal screen:', resBox)
-        self.intResolutions = internalResolutions
-        self.intResolutionsBox = QtGui.QComboBox(resBox)
-        for res in internalResolutions:
-            self.intResolutionsBox.addItem(res2user(res))
-        self.intResolutionsBox.setCurrentIndex(0) # select first resolution
-        self.intRow = makeLayout(QtGui.QHBoxLayout(), [self.intResLabel, self.intResolutionsBox])
-        # both screens
-        self.mirrorResLabel = QtGui.QLabel('Resolution of both screens:', resBox)
-        self.mirrorResolutions = commonResolutions
-        self.mirrorResolutionsBox = QtGui.QComboBox(resBox)
-        for res in commonResolutions:
-            self.mirrorResolutionsBox.addItem(res2user(res))
-        self.mirrorResolutionsBox.setCurrentIndex(0) # select first resolution
-        self.mirrorRow = makeLayout(QtGui.QHBoxLayout(), [self.mirrorResLabel, self.mirrorResolutionsBox])
-        # show them all
-        resBox.setLayout(makeLayout(QtGui.QVBoxLayout(), [self.extRow, self.intRow, self.mirrorRow]))
+        # fill relative position box
+        for pos in RelativeScreenPosition:
+            self.relPos.addItem(relPosNames[pos], pos)
         
         
-        # last row: buttons
-        buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel, QtCore.Qt.Horizontal, self)
-        buttons.accepted.connect(self.accept)
-        buttons.rejected.connect(self.reject)
-        
-        # add them all to the window
-        self.setLayout(makeLayout(QtGui.QVBoxLayout(), [posBox, self.primBox, resBox, buttons]))
-        
-        # make sure we are consistent
-        self.updateForm()
+        # keep resolutions in sync when in mirror mode
+        def syncIfMirror(source, target):
+            def _slot(idx):
+                if self.isMirror:
+                    target.setCurrentIndex(idx)
+            source.currentIndexChanged.connect(_slot)
+        syncIfMirror(self.intRes, self.extRes)
+        syncIfMirror(self.extRes, self.intRes)
+
+        # connect the update function, and make sure we are in a correct state
+        self.intEnabled.toggled.connect(self.updateEnabledControls)
+        self.extEnabled.toggled.connect(self.updateEnabledControls)
+        self.relPos.currentIndexChanged.connect(self.updateEnabledControls)
+        self.updateEnabledControls()
+    
+    def getRelativeScreenPosition(self):
+        idx = self.relPos.currentIndex()
+        return self.relPos.itemData(idx)
+    
+    def fillResolutionBox(self, box, resolutions):
+        # 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)
+        else:
+            # first clear it
+            while box.count() > 0:
+                box.removeItem(0)
+            # then fill it
+            for res in resolutions:
+                box.addItem(str(res), res)
     
     
-    def updateForm(self):
-        self.primBox.setEnabled(self.posLeft.isChecked() or self.posRight.isChecked())
-        self.extResolutionsBox.setEnabled(not self.mirror.isChecked())
-        self.extResLabel.setEnabled(not self.mirror.isChecked())
-        self.intResolutionsBox.setEnabled(self.posLeft.isChecked() or self.posRight.isChecked())
-        self.intResLabel.setEnabled(self.posLeft.isChecked() or self.posRight.isChecked())
-        self.mirrorResolutionsBox.setEnabled(self.mirror.isChecked())
-        self.mirrorResLabel.setEnabled(self.mirror.isChecked())
+    def updateEnabledControls(self):
+        intEnabled = self.intEnabled.isChecked()
+        extEnabled = self.extEnabled.isChecked()
+        bothEnabled = intEnabled and extEnabled
+        self.isMirror = bothEnabled and self.getRelativeScreenPosition() == RelativeScreenPosition.MIRROR # only if both are enabled, we can really mirror
+        # configure screen controls
+        self.intRes.setEnabled(intEnabled)
+        self.intPrimary.setEnabled(intEnabled and not self.isMirror)
+        self.extRes.setEnabled(extEnabled)
+        self.extPrimary.setEnabled(extEnabled and not self.isMirror)
+        if not intEnabled and extEnabled:
+            self.extPrimary.setChecked(True)
+        elif not extEnabled and intEnabled:
+            self.intPrimary.setChecked(True)
+        # which resolutions do we offer?
+        if self.isMirror:
+            commonRes = self._situation.commonResolutions()
+            self.fillResolutionBox(self.intRes, commonRes)
+            self.fillResolutionBox(self.extRes, commonRes)
+            self.intRes.setCurrentIndex(self.extRes.currentIndex())
+        else:
+            self.fillResolutionBox(self.intRes, self._situation.internalResolutions())
+            self.fillResolutionBox(self.extRes, self._situation.externalResolutions())
+        # configure position control
+        self.posGroup.setEnabled(bothEnabled)
+        self.posLabel1.setEnabled(bothEnabled)
+        self.posLabel2.setEnabled(bothEnabled)
+        self.relPos.setEnabled(bothEnabled)
+        # avoid having no screen
+        self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(intEnabled or extEnabled)
     
     def run(self):
         self.exec_()
         if not self.result(): return None
     
     def run(self):
         self.exec_()
         if not self.result(): return None
-        if self.mirror.isChecked():
-            return ScreenSetup(RelativeScreenPosition.MIRROR,
-                self.mirrorResolutions[self.mirrorResolutionsBox.currentIndex()],
-                self.mirrorResolutions[self.mirrorResolutionsBox.currentIndex()],
-                extIsPrimary = True)
-        else:
-            return ScreenSetup(self.getRelativeScreenPosition(),
-                self.intResolutions[self.intResolutionsBox.currentIndex()],
-                self.extResolutions[self.extResolutionsBox.currentIndex()],
-                self.primExt.isChecked())
-    
-    def getRelativeScreenPosition(self):
-        if self.posLeft.isChecked():
-            return RelativeScreenPosition.LEFT
-        elif self.posRight.isChecked():
-            return RelativeScreenPosition.RIGHT
-        elif self.extOnly.isChecked():
-            return RelativeScreenPosition.EXTERNAL_ONLY
-        else:
-            raise Exception("Nothing is checked?")
+        intRes = self.intRes.itemData(self.intRes.currentIndex()) if self.intEnabled.isChecked() else None
+        extRes = self.extRes.itemData(self.extRes.currentIndex()) if self.extEnabled.isChecked() else None
+        return ScreenSetup(intRes, extRes, self.getRelativeScreenPosition(), self.extPrimary.isChecked())
diff --git a/qt_dialogue.ui b/qt_dialogue.ui
new file mode 100644 (file)
index 0000000..6b52c88
--- /dev/null
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Dialog</class>
+ <widget class="QDialog" name="Dialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>332</width>
+    <height>217</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>DSL - easy Display Setup for Laptops</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Screen Configuratioon</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <widget class="QLabel" name="label">
+        <property name="text">
+         <string>Enable:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QCheckBox" name="intEnabled">
+        <property name="text">
+         <string>Internal</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="2">
+       <widget class="QCheckBox" name="extEnabled">
+        <property name="text">
+         <string>External</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label_2">
+        <property name="text">
+         <string>Resolution:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <widget class="QComboBox" name="intRes"/>
+      </item>
+      <item row="1" column="2">
+       <widget class="QComboBox" name="extRes"/>
+      </item>
+      <item row="2" column="0">
+       <widget class="QLabel" name="label_3">
+        <property name="text">
+         <string>Primary:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="1">
+       <widget class="QRadioButton" name="intPrimary">
+        <property name="text">
+         <string>Internal</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="2">
+       <widget class="QRadioButton" name="extPrimary">
+        <property name="text">
+         <string>External</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="posGroup">
+     <property name="title">
+      <string>Screen Position</string>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <item>
+       <widget class="QLabel" name="posLabel1">
+        <property name="text">
+         <string>External screen is</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QComboBox" name="relPos"/>
+      </item>
+      <item>
+       <widget class="QLabel" name="posLabel2">
+        <property name="text">
+         <string>internal screen.</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>1</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>Dialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>Dialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/screen.py b/screen.py
new file mode 100644 (file)
index 0000000..a543fea
--- /dev/null
+++ b/screen.py
@@ -0,0 +1,210 @@
+#!/usr/bin/python3
+# DSL - easy Display Setup for Laptops
+# Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+import re, subprocess
+from enum import Enum
+
+## utility functions
+
+# execute a process, return output as iterator, throw exception if there was an error
+# you *must* iterate to the end if you use this!
+def processOutputGen(*args):
+    with subprocess.Popen(args, stdout=subprocess.PIPE) as p:
+        for line in p.stdout:
+            yield line.decode("utf-8")
+    if p.returncode != 0:
+        raise Exception("Error executing "+str(args))
+def processOutputIt(*args):
+    return list(processOutputGen(*args)) # list() iterates over the generator
+
+## the classes
+
+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
+
+
+class Resolution:
+    '''Represents a resolution of a screen'''
+    def __init__(self, width, height):
+        self.width = width
+        self.height = height
+    
+    def __eq__(self, other):
+        if not isinstance(other, Resolution):
+            return False
+        return self.width == other.width and self.height == other.height
+    
+    def __ne__(self, other):
+        return not self.__eq__(other)
+    
+    def __str__(self):
+        # get ratio
+        ratio = int(round(16.0*self.height/self.width))
+        if ratio == 12: # 16:12 = 4:3
+            strRatio = '4:3'
+        elif ratio == 13: # 16:12.8 = 5:4
+            strRatio = '5:4'
+        else: # let's just hope this will never be 14 or more...
+            strRatio = '16:%d' % ratio
+        return '%dx%d (%s)' %(self.width, self.height, strRatio)
+    
+    def __repr__(self):
+        return 'screen.Resolution('+self.forXrandr()+')'
+    
+    def forXrandr(self):
+        return str(self.width)+'x'+str(self.height)
+
+
+class ScreenSetup:
+    '''Represents a screen configuration (relative to some notion of an "internal" and an "external" screen): Which screens are enabled with which resolution, how
+       are they positioned, which is the primary screen.'''
+    def __init__(self, intResolution, extResolution, relPosition = None, extIsPrimary = True):
+        '''The resolutions can be None to disable the screen, instances of Resolution. The last two arguments only matter if both screens are enabled.'''
+        assert intResolution is None or isinstance(intResolution, Resolution)
+        assert extResolution is None or isinstance(extResolution, Resolution)
+        
+        self.intResolution = intResolution
+        self.extResolution = extResolution
+        self.relPosition = relPosition
+        self.extIsPrimary = extIsPrimary or self.intResolution is None # external is always primary if it is the only one
+    
+    def getInternalArgs(self):
+        if self.intResolution is None:
+            return ["--off"]
+        args = ["--mode", self.intResolution.forXrandr()] # set internal screen to desired resolution
+        if not self.extIsPrimary:
+            args.append('--primary')
+        return args
+    
+    def getExternalArgs(self, intName):
+        if self.extResolution is None:
+            return ["--off"]
+        args = ["--mode", self.extResolution.forXrandr()] # set external screen to desired resolution
+        if self.extIsPrimary:
+            args.append('--primary')
+        if self.intResolution is None:
+            return args
+        # set position
+        args += [{
+                RelativeScreenPosition.LEFT  : '--left-of',
+                RelativeScreenPosition.RIGHT : '--right-of',
+                RelativeScreenPosition.ABOVE : '--above',
+                RelativeScreenPosition.BELOW : '--below',
+                RelativeScreenPosition.MIRROR: '--same-as',
+            }[self.relPosition], intName]
+        return args
+
+
+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
+    
+    '''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):
+        '''Both arguments are lists of connector names. The first one which exists and has a screen attached is chosen for that class. <externalConnectorNames> can be None to
+           just choose any remaining connector.'''
+        # which connectors are there?
+        self._getXrandrInformation()
+        print(self.connectors)
+        # 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)
+        # and the external one
+        if externalConnectorNames is None:
+            externalConnectorNames = list(self.connectors.keys())
+            externalConnectorNames.remove(self.internalConnector)
+        self.externalConnector = self._findAvailableConnector(externalConnectorNames)
+        print(self.externalConnector)
+    
+    # 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"):
+            # screen?
+            m = re.search(r'^Screen [0-9]+: ', line)
+            if m is not None: # ignore this line
+                connector = None
+                continue
+            # 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] = []
+                continue
+            # new resolution?
+            m = re.search(r'^   ([\d]+)x([\d]+) +', line)
+            if m is not None:
+                resolution = Resolution(int(m.groups()[0]), int(m.groups()[1]))
+                assert connector is not None
+                self.connectors[connector].append(resolution)
+                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)
+    
+    # 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 None
+    
+    # return available internal resolutions
+    def internalResolutions(self):
+        return self.connectors[self.internalConnector]
+    
+    # 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 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]
+    
+    # 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"]
+        # set arguments for the relevant ones
+        connectorArgs[self.internalConnector] = setup.getInternalArgs()
+        if self.externalConnector is not None:
+            connectorArgs[self.externalConnector] = setup.getExternalArgs(self.internalConnector)
+        else:
+            assert setup.extResolution is None, "There's no external screen to set a resolution for"
+        # now compose the arguments
+        call = ["xrandr"]
+        for name in connectorArgs:
+            call += ["--output", name] + connectorArgs[name]
+        return call
+
index e926b3a0efdd65d4806f5309e92660a208140dce..b5e72d07d7f331b1cb77255f221f5859c160f9e4 100644 (file)
@@ -1,5 +1,6 @@
 # DSL - easy Display Setup for Laptops
 # DSL - easy Display Setup for Laptops
-# Copyright (C) 2012 Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012      Ralf Jung <post@ralfj.de>
+# Copyright (C) 2012-2015 Constantin Berhard<constantin@exxxtremesys.lu>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -15,7 +16,7 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 
-from dsl import RelativeScreenPosition, ScreenSetup, res2user, processOutputIt
+from screen import RelativeScreenPosition, ScreenSetup, processOutputIt
 
 def userChoose (title, choices, returns, fallback):
     assert len(choices) == len(returns)
 
 def userChoose (title, choices, returns, fallback):
     assert len(choices) == len(returns)
@@ -37,16 +38,16 @@ def run (internalResolutions, externalResolutions):
     extres = externalResolutions[0]
     extprim = None
     if relpos != RelativeScreenPosition.EXTERNAL_ONLY:
     extres = externalResolutions[0]
     extprim = None
     if relpos != RelativeScreenPosition.EXTERNAL_ONLY:
-        intres = userChoose ("internal display resolution", list(map(res2user,internalResolutions)), internalResolutions, None)
+        intres = userChoose ("internal display resolution", list(map(str,internalResolutions)), internalResolutions, None)
         if intres == None:
             return None
     else:
         extprim = True
         if intres == None:
             return None
     else:
         extprim = True
-    extres = userChoose ("external display resolution", list(map(res2user,externalResolutions)), externalResolutions, None)
+    extres = userChoose ("external display resolution", list(map(str,externalResolutions)), externalResolutions, None)
     if extres == None:
         return None
     if extprim == None:
         extprim = userChoose ("Which display should be the primary display?", ["internal display", "external display"], [False, True], None)
     if extprim == None:
         return None
     if extres == None:
         return None
     if extprim == None:
         extprim = userChoose ("Which display should be the primary display?", ["internal display", "external display"], [False, True], None)
     if extprim == None:
         return None
-    return ScreenSetup(relpos,intres,extres,extprim)
+    return ScreenSetup(intres,extres,relpos,extprim)