diff --git a/monitorDaemon.py b/monitorDaemon.py --- a/monitorDaemon.py +++ b/monitorDaemon.py @@ -1,445 +1,480 @@ #!/usr/bin/env python2 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 +from PyQt4.QtCore import QThread, QTimer, pyqtSignal, QObject, QMetaObject, Qt, pyqtSlot from PyQt4 import QtGui DEFAULT_TIMEOUT = 250 +GRACE_TIMEOUT = 5000 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) def screenInformationChanged(self): 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: continue 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): 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) 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') 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('Screen change') - print(e._data) + # 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 - self.timer.stop() - self.timer.start(DEFAULT_TIMEOUT) + if not self.graceTimer.isActive(): + self.timer.stop() + self.timer.start(DEFAULT_TIMEOUT) + else: + print('gracePeriod') # 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) config = ConfigParser.ConfigParser() config.add_section('General') - config.set('General', 'numEntries', '0') + 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: print(self.dic[name]) #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 saveConfiguration(self, screenConfiguration): 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 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) class Manager(QObject): applyConfig = pyqtSignal([dict]) def __init__(self, config, xri, parent = None): QObject.__init__(self) self.config = config self.parent = parent self.xri = xri #self.xri.start() def saveConfig(self): screenConfig = self.xri.getScreenConfiguration() self.config.saveConfiguration(screenConfig) def loadAndApply(self): print('loadAndApply') screenConfig = self.xri.getScreenConfiguration() storedConfig, name = self.config.getConfig(screenConfig) print(storedConfig) if storedConfig: self.applyConfig.emit(storedConfig) print('done') + + @pyqtSlot() + def loadOnStartup(self): + if self.config.loadOnStartupEnabled(): + self.loadAndApply() def screenConfigurationChanged(self): print('Slot Received') 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() - break + else: + if self.config.autosaveEnabled(): + print('Autosave') + self.saveConfig() print('done.') 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()