diff --git a/.hgignore b/.hgignore new file mode 100644 --- /dev/null +++ b/.hgignore @@ -0,0 +1,19 @@ +glob:.eric6project +glob:_eric6project +glob:.eric5project +glob:_eric5project +glob:.eric4project +glob:_eric4project +glob:.ropeproject +glob:_ropeproject +glob:.directory +glob:**.pyc +glob:**.pyo +glob:**.orig +glob:**.bak +glob:**.rej +glob:**~ +glob:cur +glob:tmp +glob:__pycache__ +glob:**.DS_Store diff --git a/monitorDaemon.e4p b/monitorDaemon.e4p new file mode 100644 --- /dev/null +++ b/monitorDaemon.e4p @@ -0,0 +1,149 @@ + + + + + + + en_US + 10ffdbf1e382339a60432c85bacb071b04891877 + Python2 + Qt4 + 0.1 + + + + + __init__.py + monitorDaemon.py + xrandr.py + + + + + + + .hgignore + + monitorDaemon.py + + Mercurial + + + + add + + + + + + + + checkout + + + + + + + + commit + + + + + + + + diff + + + + + + + + export + + + + + + + + global + + + + + + + + history + + + + + + + + log + + + + + + + + remove + + + + + + + + status + + + + + + + + tag + + + + + + + + update + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/monitorDaemon.py b/monitorDaemon.py --- a/monitorDaemon.py +++ b/monitorDaemon.py @@ -1,310 +1,455 @@ -#!/usr/bin/env python -import subprocess +#!/usr/bin/env python2 import os import hashlib -import binascii -import re import sys -import configparser -from Xlib import X, display, Xutil +import ConfigParser +from Xlib import X, display from Xlib.ext import randr +from PyQt4.QtCore import QThread, QTimer, pyqtSignal, QObject +from PyQt4 import QtGui # TODO: We could move away from outputs completely # and make configuration EDID-based only -class xrandrInterface: +DEFAULT_TIMEOUT = 500 + +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.window.xrandr_select_input( - # randr.RRScreenChangeNotifyMask - # | randr.RRCrtcChangeNotifyMask - # | randr.RROutputChangeNotifyMask - # | randr.RROutputPropertyNotifyMask - # ) 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 - #print(self.display.xrandr_list_output_properties(output)._data) - """ - print(self.outputStatus) - op = self.getOutputForOutputname('eDP1') - print(op) - md = self.findModeByName(op, '1920x1080') - print(md) - cr = self.getUsableCrtcForOutput(op) - if not cr: - #print('Disabling CRTC') - cr = self.getCurrentCrtcForOutput(op) - self.disableCrtc(cr) - else: - #print('Enabling CRTC') - print(cr) - self.configureCrtcForOutputAndMode(cr, op, md, 0, 0) - """ def applyConfiguration(self, config): - # FIXME: # Rework to use getOutputForEdid and applying # configuration based on EDID only resources = self.window.xrandr_get_screen_resources()._data outputs = resources['outputs'] for output in outputs: info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data name = info['name'] found = False - for nm in config: - if nm == name and config[nm]['modename'] != '0': + for ii in range(0, int(config['edidcount'])): + if config['output' + str(ii+1)] == name and config['mode' + str(ii+1)] != '0': found = True if not found: crtc = self.getCurrentCrtcForOutput(output) if crtc: self.disableCrtc(crtc) - for nm in config: - if config[nm]['modename'] != '0': - output = self.getOutputForOutputname(nm) + for ii in range(0, int(config['edidcount'])): + if config['mode' + str(ii+1)] != '0': + output = self.getOutputForOutputname(config['output' + str(ii+1)]) crtc = self.getCurrentCrtcForOutput(output) if not crtc: crtc = self.getUsableCrtcForOutput(output) - mode = self.findModeByName(output, config[nm]['modename']) - self.configureCrtcForOutputAndMode(crtc, output, mode, config[nm]['posx'], config[nm]['posy']) + 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'] + if w > width: + width = w + if h > height: + height = h + 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 # 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 + elif 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) 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 = 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 = ConfigParser.ConfigParser() config.read(self.cfgfile) for section in config.sections(): dic[section] = {} - for key in config[section]: - dic[section][key] = config.get(section, key) + for key, value in config.items(section): + dic[section][key] = value self.dic = dic return dic def dic2cfg(self): - config = configparser.ConfigParser() + 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] - return None + print(self.dic[name]) + #for jj in range(0, len(screenConfiguration)): + return (self.dic[name], name) + return (None, None) def saveConfiguration(self, screenConfiguration): - ne = int(self.dic['General']['numentries']) - ne += 1 - self.dic['General']['numentries'] = str(ne) - name = 'Config' + str(ne) + 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') + + 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() + -settingsdir = os.path.join(os.environ['HOME'], '.config') -cfgfile = os.path.join(settingsdir, 'monitorDaemon.ini') -cm = ConfigManager(cfgfile) +def main(): + app = QtGui.QApplication(sys.argv) + app.setQuitOnLastWindowClosed(False); -cfg = cm.getDic() -#print(cfg) - -xri = xrandrInterface() -screenConfig = xri.getScreenConfiguration() -#print(screenConfig) + settingsdir = os.path.join(os.environ['HOME'], '.config') + cfgfile = os.path.join(settingsdir, 'monitorDaemon.ini') + cm = ConfigManager(cfgfile) + + w = QtGui.QWidget() + x = xrandrInterface() -storedConfig = cm.getConfig(screenConfig) + m = Manager(cm, x, w) + x.screenConfigChanged.connect(m.screenConfigurationChanged) + m.applyConfig.connect(x.applyConfiguration) + + trayIcon = SystemTrayIcon(QtGui.QIcon("/usr/share/icons/monitorDaemon.png"), m, w) -if storedConfig: - print('LoadAndApply') - xri.applyConfiguration(screenConfig) -else: - print('SaveConfig') - cm.saveConfiguration(screenConfig) + trayIcon.show() + sys.exit(app.exec_()) -sys.exit(0) - +if __name__ == '__main__': + main()