diff --git a/monitorDaemon.py b/monitorDaemon.py --- a/monitorDaemon.py +++ b/monitorDaemon.py @@ -1,587 +1,599 @@ #!/usr/bin/env python2 # monitorDaemon.py # (c) 2017 Andreas Boehler # This file is free software; you can redistribute it and/or modify # it under the terms of either the GNU General Public License version 2 # or the GNU Lesser General Public License version 2.1, both as # published by the Free Software Foundation. import os import hashlib import sys import ConfigParser from Xlib import display from Xlib.ext import randr from PyQt4.QtCore import QThread, QTimer, pyqtSignal, QObject, QMetaObject, Qt, pyqtSlot from PyQt4 import QtGui import time +import logging as log -DEFAULT_TIMEOUT = 250 +log.basicConfig(format="%(levelname)s:%(message)s", level=log.ERROR) +#log.basicConfig(format="%(levelname)s: %(message)s", level=log.DEBUG) +log.info("Verbose output.") + +DEFAULT_TIMEOUT = 500 GRACE_TIMEOUT = 5000 +VERBOSE = True class xrandrInterface(QThread): screenConfigChanged = pyqtSignal() def __init__(self): QThread.__init__(self) self.display = display.Display() self.screen = self.display.screen() self.running = True self.window = self.screen.root.create_window(0, 0, 1, 1, 1, self.screen.root_depth, #event_mask = (X.ExposureMask | #X.StructureNotifyMask | #X.ButtonPressMask | #X.ButtonReleaseMask | #X.Button1MotionMask) ) self.outputStatus = {} self.oldOutputStatus = {} self.crtcStatus = {} self.getScreenInformation() self.enableEvents() self.timer = QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(self.screenInformationChanged) self.graceTimer = QTimer() self.graceTimer.setSingleShot(True) @pyqtSlot() def screenInformationChanged(self): + log.info("screenInformationChanged") self.getScreenInformation() self.screenConfigChanged.emit() def disableEvents(self): self.window.xrandr_select_input(0) def enableEvents(self): self.window.xrandr_select_input( randr.RRScreenChangeNotifyMask #| randr.RRCrtcChangeNotifyMask #| randr.RROutputChangeNotifyMask #| randr.RROutputPropertyNotifyMask ) def getScreenInformation(self): self.oldOutputStatus = self.outputStatus resources = self.window.xrandr_get_screen_resources()._data outputs = resources['outputs'] self.outputStatus = {} self.crtcStatus = {} for crtc in resources['crtcs']: crtcinfo = self.display.xrandr_get_crtc_info(crtc, resources['config_timestamp'])._data self.crtcStatus[crtc] = {} self.crtcStatus[crtc]['x'] = crtcinfo['x'] self.crtcStatus[crtc]['y'] = crtcinfo['y'] self.crtcStatus[crtc]['mode'] = crtcinfo['mode'] self.crtcStatus[crtc]['outputs'] = crtcinfo['outputs'] self.crtcStatus[crtc]['possible_outputs'] = crtcinfo['possible_outputs'] modedata = self.parseModes(resources['mode_names'], resources['modes']) for output in outputs: info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data if info['connection'] != 0: continue edid = self.get_edid(output) name = info['name'] crtc = info['crtc'] if crtc == 0: modename = '0' posx = '0' posy = '0' else: mode = self.crtcStatus[crtc]['mode'] modename = modedata[mode]['name'] posx = self.crtcStatus[crtc]['x'] posy = self.crtcStatus[crtc]['y'] self.outputStatus[name] = {} self.outputStatus[name]['edid'] = edid self.outputStatus[name]['crtc'] = crtc self.outputStatus[name]['modename'] = modename self.outputStatus[name]['posx'] = posx self.outputStatus[name]['posy'] = posy def applyConfiguration(self, config): + log.info("applyConfiguration") resources = self.window.xrandr_get_screen_resources()._data outputs = resources['outputs'] # Find all available outputs and disable outputs # not present in the config for output in outputs: found = False for ii in range(0, int(config['edidcount'])): edid = self.get_edid(output) if edid: if config['edid' + str(ii+1)] == edid and config['mode' + str(ii+1)] != '0': found = True if not found: crtc = self.getCurrentCrtcForOutput(output) if crtc: self.disableCrtc(crtc) self.graceTimer.start(GRACE_TIMEOUT) # Apply stored screen configuration, based on EDID for ii in range(0, int(config['edidcount'])): if config['mode' + str(ii+1)] != '0': output = self.getOutputForEdid(config['edid' + str(ii+1)]) crtc = self.getCurrentCrtcForOutput(output) if not crtc: crtc = self.getUsableCrtcForOutput(output) mode = self.findModeByName(output, config['mode' + str(ii+1)]) self.configureCrtcForOutputAndMode(crtc, output, mode, int(config['posx' + str(ii+1)]), int(config['posy' + str(ii+1)])) self.updateScreenSize() self.getScreenInformation() def updateScreenSize(self): # get all CRTC configs and calculate the required screen size # set screen size afterwards resources = self.window.xrandr_get_screen_resources()._data outputs = resources['outputs'] width = 0 height = 0 for output in outputs: oi = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data crtc = oi['crtc'] if crtc: info = self.display.xrandr_get_crtc_info(crtc, resources['config_timestamp'])._data w = info['x'] + info['width'] h = info['y'] + info['height'] if w > width: width = w if h > height: height = h # Do the same as the other RandR implementations: set width/height in mm # so that we match 96 dpi. mm_width = (width / 96.0) * 25.4 + 0.5; mm_height = (height / 96.0) * 25.4 + 0.5; self.window.xrandr_set_screen_size(width, height, mm_width, mm_height) def getScreenConfiguration(self): return self.outputStatus def getOldScreenConfiguration(self): return self.oldOutputStatus def disableCrtc(self, crtc): resources = self.window.xrandr_get_screen_resources()._data self.display.xrandr_set_crtc_config(crtc, resources['config_timestamp'], 0, 0, 0, randr.Rotate_0, []) def getCurrentCrtcForOutput(self, output): resources = self.window.xrandr_get_screen_resources()._data info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data return info['crtc'] def configureCrtcForOutputAndMode(self, crtc, output, mode, posx, posy): resources = self.window.xrandr_get_screen_resources()._data self.display.xrandr_set_crtc_config(crtc, resources['config_timestamp'], posx, posy, mode, randr.Rotate_0, [output]) def getOutputForOutputname(self, name): resources = self.window.xrandr_get_screen_resources()._data for output in resources['outputs']: info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data if info['name'] == name: return output return None def getUsableCrtcForOutput(self, output): resources = self.window.xrandr_get_screen_resources()._data for crtc in resources['crtcs']: crtcinfo = self.display.xrandr_get_crtc_info(crtc, resources['config_timestamp'])._data if len(crtcinfo['outputs']) == 0: if output in crtcinfo['possible_outputs']: return crtc return None def findModeByName(self, output, modename): resources = self.window.xrandr_get_screen_resources()._data modedata = self.parseModes(resources['mode_names'], resources['modes']) info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data modes = info['modes'] for mode in modes: if modedata[mode]['name'] == modename: return mode return None def parseModes(self, mode_names, modes): lastIdx = 0 modedatas = dict() for mode in modes: modedata = dict(mode._data) modedata['name'] = mode_names[lastIdx:lastIdx + modedata['name_length']] modedatas[modedata['id']] = modedata lastIdx += modedata['name_length'] return modedatas def getOutputForEdid(self, edid): resources = self.window.xrandr_get_screen_resources()._data for output in resources['outputs']: medid = self.get_edid(output) if edid == medid: return output return None # query the edid module for output_nr def get_edid(self, output_nr): PROPERTY_EDID = self.display.intern_atom("EDID", 0) INT_TYPE = 19 props = self.display.xrandr_list_output_properties(output_nr) if PROPERTY_EDID in props.atoms: try: rawedid = self.display.xrandr_get_output_property(output_nr, PROPERTY_EDID, INT_TYPE, 0, 400) except: - print("Error loading EDID data of output ", output_nr) + log.error("Error loading EDID data of output ", output_nr) return None edidstream = rawedid._data['value'] hx = bytearray() hx.extend(edidstream) hs = hashlib.sha1(hx).hexdigest() return hs else: return None def run(self): while self.running: - #print('running') ev = self.display.pending_events() if(ev == 0): time.sleep(0.001) continue e = self.display.next_event() # Window has been destroyed, quit #if e.type == X.DestroyNotify: # sys.exit(0) # Screen information has changed if e.type == self.display.extension_event.ScreenChangeNotify: # print(e._data) # Trigger a QTimer here for a few seconds that # then updates the screen information and # runs the update code # If a timer is running, reset it to the default if not self.graceTimer.isActive(): + log.info("Starting Timer...") self.timer.stop() self.timer.start(DEFAULT_TIMEOUT) # CRTC information has changed #elif e.type == self.display.extension_event.CrtcChangeNotify: # print('CRTC change') # print(e._data) # Output information has changed #elif e.type == self.display.extension_event.OutputChangeNotify: # print('Output change') # print(e._data) # Output property information has changed #elif e.type == self.display.extension_event.OutputPropertyNotify: # print('Output property change') # print(e._data) # Somebody wants to tell us something #elif e.type == X.ClientMessage: # if e.client_type == self.WM_PROTOCOLS: # fmt, data = e.data # if fmt == 32 and data[0] == self.WM_DELETE_WINDOW: # sys.exit(0) class ConfigManager: def __init__(self, cfgfile): self.cfgfile = cfgfile if not os.path.exists(cfgfile): self.recreateCfg() self.dic = {} self.cfg2dic() def getDic(self): return self.dic def recreateCfg(self): - print('Creating new config file: ' + self.cfgfile) + log.info('Creating new config file: ' + self.cfgfile) config = ConfigParser.ConfigParser() config.add_section('General') config.set('General', 'numentries', '0') config.set('General', 'autosave', '0') config.set('General', 'loadonstartup', '1') with open(self.cfgfile, 'w') as configfile: config.write(configfile) def cfg2dic(self): dic = {} config = ConfigParser.ConfigParser() config.read(self.cfgfile) for section in config.sections(): dic[section] = {} for key, value in config.items(section): dic[section][key] = value self.dic = dic return dic def dic2cfg(self): config = ConfigParser.ConfigParser() for section in self.dic: config.add_section(section) for key in self.dic[section]: config.set(section, key, self.dic[section][key]) with open(self.cfgfile, 'w') as configfile: config.write(configfile) def getConfig(self, screenConfiguration): ne = int(self.dic['General']['numentries']) for ii in range(0, ne): name = 'Config' + str(ii+1) if int(self.dic[name]['edidcount']) != len(screenConfiguration): continue found = True for outputName in screenConfiguration: lFound = False for jj in range(0, len(screenConfiguration)): if self.dic[name]['edid' + str(jj+1)] == screenConfiguration[outputName]['edid']: lFound = True if lFound and found: found = True else: found = False if found: #for jj in range(0, len(screenConfiguration)): return (self.dic[name], name) return (None, None) def autosaveEnabled(self): if self.dic['General']['autosave'] == '1': return True return False def loadOnStartupEnabled(self): if self.dic['General']['loadonstartup'] == '1': return True return False def getApplicationConfig(self): return self.dic['General'] def saveApplicationConfig(self, cfg): ne = self.dic['General'] for key in cfg: self.dic['General'][key] = cfg[key] self.dic2cfg() def saveConfiguration(self, screenConfiguration): + log.info("saveConfiguration") oldConfig, oldname = self.getConfig(screenConfiguration) if oldConfig: name = oldname else: ne = int(self.dic['General']['numentries']) ne += 1 self.dic['General']['numentries'] = str(ne) name = 'Config' + str(ne) self.dic[name] = {} self.dic[name]['edidcount'] = str(len(screenConfiguration)) cnt = 1 for outputName in screenConfiguration.keys(): self.dic[name]['edid'+str(cnt)] = screenConfiguration[outputName]['edid'] self.dic[name]['output'+str(cnt)] = outputName self.dic[name]['mode'+str(cnt)] = screenConfiguration[outputName]['modename'] self.dic[name]['posx'+str(cnt)] = str(screenConfiguration[outputName]['posx']) self.dic[name]['posy'+str(cnt)] = str(screenConfiguration[outputName]['posy']) cnt += 1 self.dic2cfg() return True class SettingsGui(QtGui.QWidget): loadSettings = pyqtSignal() saveSettings = pyqtSignal([dict]) def __init__(self, parent=None): self.parent = parent QtGui.QWidget.__init__(self, parent) self.setWindowTitle('monitorDaemon Settings GUI') grid = QtGui.QGridLayout() self.setLayout(grid) lbl1 = QtGui.QLabel('Load on Startup') lbl2 = QtGui.QLabel('AutoSave') self.cbAutoSave = QtGui.QCheckBox() self.cbLoadOnStartup = QtGui.QCheckBox() btn1 = QtGui.QPushButton('Save') btn2 = QtGui.QPushButton('Quit') grid.addWidget(lbl1, 1, 1) grid.addWidget(self.cbLoadOnStartup, 1, 2) grid.addWidget(lbl2, 2, 1) grid.addWidget(self.cbAutoSave, 2, 2) grid.addWidget(btn1, 3, 1) grid.addWidget(btn2, 3, 2) btn1.clicked.connect(self.saveBtnClicked) btn2.clicked.connect(self.quit) @pyqtSlot() def quit(self): QtGui.qApp.quit() @pyqtSlot() def saveBtnClicked(self): cfg = {} if self.cbAutoSave.isChecked(): cfg['autosave'] = '1' else: cfg['autosave'] = '0' if self.cbLoadOnStartup.isChecked(): cfg['loadonstartup'] = '1' else: cfg['loadonstartup'] = '0' self.saveSettings.emit(cfg) def toggleVisibility(self, reason): if reason != QtGui.QSystemTrayIcon.Trigger: return if self.isVisible(): self.hide() else: self.loadSettings.emit() self.show() @pyqtSlot(dict) def settingsLoaded(self, settings): if settings['autosave'] == '1': self.cbAutoSave.setChecked(True) else: self.cbAutoSave.setChecked(False) if settings['loadonstartup'] == '1': self.cbLoadOnStartup.setChecked(True) else: self.cbLoadOnStartup.setChecked(False) class SystemTrayIcon(QtGui.QSystemTrayIcon): def __init__(self, icon, m, parent=None): self.parent = parent QtGui.QSystemTrayIcon.__init__(self, icon, parent) menu = QtGui.QMenu(parent) saveAction = menu.addAction("Save Current Configuration") saveAction.triggered.connect(m.saveConfig) loadAction = menu.addAction("Load and Apply Configuration") loadAction.triggered.connect(m.loadAndApply) exitAction = menu.addAction("Exit") exitAction.triggered.connect(m.quit) self.setContextMenu(menu) self.settingsGui = SettingsGui() self.activated.connect(self.settingsGui.toggleVisibility) self.settingsGui.loadSettings.connect(m.loadSettings) self.settingsGui.saveSettings.connect(m.saveSettings) m.settingsLoaded.connect(self.settingsGui.settingsLoaded) class Manager(QObject): applyConfig = pyqtSignal([dict]) settingsLoaded = pyqtSignal([dict]) def __init__(self, config, xri, parent = None): QObject.__init__(self) self.config = config self.parent = parent self.xri = xri #self.xri.start() @pyqtSlot() def saveConfig(self): screenConfig = self.xri.getScreenConfiguration() self.config.saveConfiguration(screenConfig) @pyqtSlot() def loadAndApply(self): + log.info("loadAndApply") screenConfig = self.xri.getScreenConfiguration() storedConfig, name = self.config.getConfig(screenConfig) if storedConfig: self.applyConfig.emit(storedConfig) @pyqtSlot(dict) def saveSettings(self, cfg): self.config.saveApplicationConfig(cfg) @pyqtSlot() def loadSettings(self): cfg = self.config.getApplicationConfig() self.settingsLoaded.emit(cfg) @pyqtSlot() def loadOnStartup(self): + log.info("loadOnStartup") if self.config.loadOnStartupEnabled(): self.loadAndApply() @pyqtSlot() def screenConfigurationChanged(self): + log.info("screenConfigurationChanged") oldConfig = self.xri.getOldScreenConfiguration() newConfig = self.xri.getScreenConfiguration() if len(oldConfig.keys()) != len(newConfig.keys()): self.loadAndApply() else: load = False # Check every output, if we encounter one with a changed # EDID, then we need to load and apply the configuration # otherwise, nothing has changed - save it? for output in oldConfig: if newConfig.has_key(output): if oldConfig[output]['edid'] == newConfig[output]['edid']: continue load = True break if load: self.loadAndApply() else: if self.config.autosaveEnabled(): self.saveConfig() @pyqtSlot() def quit(self): QtGui.qApp.quit() def main(): app = QtGui.QApplication(sys.argv) app.setQuitOnLastWindowClosed(False); settingsdir = os.path.join(os.environ['HOME'], '.config') cfgfile = os.path.join(settingsdir, 'monitorDaemon.ini') cm = ConfigManager(cfgfile) w = QtGui.QWidget() x = xrandrInterface() m = Manager(cm, x, w) x.screenConfigChanged.connect(m.screenConfigurationChanged) m.applyConfig.connect(x.applyConfiguration) QMetaObject.invokeMethod(x, "start", Qt.QueuedConnection) QMetaObject.invokeMethod(m, "loadOnStartup", Qt.QueuedConnection) # Wait for up to 30 seconds for the systemTray to become available (required for, e.g., Xfce) count = 0 while not QtGui.QSystemTrayIcon.isSystemTrayAvailable() and count < 30: time.sleep(1) count += 1 trayIcon = SystemTrayIcon(QtGui.QIcon("/usr/share/icons/monitorDaemon.png"), m, w) trayIcon.show() sys.exit(app.exec_()) if __name__ == '__main__': main()