diff --git a/monitorDaemon.py b/monitorDaemon.py --- a/monitorDaemon.py +++ b/monitorDaemon.py @@ -1,455 +1,439 @@ #!/usr/bin/env python2 import os import hashlib import sys import ConfigParser -from Xlib import X, display +from Xlib import display from Xlib.ext import randr -from PyQt4.QtCore import QThread, QTimer, pyqtSignal, QObject +from PyQt4.QtCore import QThread, QTimer, pyqtSignal, QObject, QMetaObject, Qt from PyQt4 import QtGui -# TODO: We could move away from outputs completely -# and make configuration EDID-based only - -DEFAULT_TIMEOUT = 500 +DEFAULT_TIMEOUT = 250 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) + #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) 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]['width'] = crtcinfo['width'] - #self.crtcStatus[crtc]['height'] = crtcinfo['height'] 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']) - #print(self.crtcStatus) for output in outputs: info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data - #print(info) 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): - # Rework to use getOutputForEdid and applying - # configuration based on EDID only 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: - info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data - name = info['name'] found = False for ii in range(0, int(config['edidcount'])): - if config['output' + str(ii+1)] == name and config['mode' + str(ii+1)] != '0': - found = True + 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) + # Apply stored screen configuration, based on EDID for ii in range(0, int(config['edidcount'])): if config['mode' + str(ii+1)] != '0': - output = self.getOutputForOutputname(config['output' + str(ii+1)]) + 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 - mm_width = 0 - mm_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 - wmm = oi['mm_width'] - hmm = oi['mm_height'] - if info['x'] != 0: - mm_width += wmm - else: - if wmm > mm_width: - mm_width = wmm - if info['y'] != 0: - mm_height += hmm - else: - if hmm > mm_height: - mm_height = hmm w = info['x'] + info['width'] - h = info['y'] = info['height'] + 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 0 + 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) + #if e.type == X.DestroyNotify: + # sys.exit(0) # Screen information has changed - elif e.type == self.display.extension_event.ScreenChangeNotify: + if e.type == self.display.extension_event.ScreenChangeNotify: print('Screen change') 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.getScreenInformation() 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) + #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') 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 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() + #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') def screenConfigurationChanged(self): print('Slot Received') oldConfig = self.xri.getOldScreenConfiguration() newConfig = self.xri.getScreenConfiguration() if len(oldConfig.keys()) != len(newConfig.keys()): self.loadAndApply() else: for output in oldConfig: if newConfig.has_key(output): if oldConfig[output]['edid'] == newConfig[output]['edid']: continue self.loadAndApply() break 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) trayIcon = SystemTrayIcon(QtGui.QIcon("/usr/share/icons/monitorDaemon.png"), m, w) trayIcon.show() sys.exit(app.exec_()) if __name__ == '__main__': main()