diff --git a/monitorDaemon.py b/monitorDaemon.py --- a/monitorDaemon.py +++ b/monitorDaemon.py @@ -1,979 +1,1002 @@ #!/usr/bin/env python # monitorDaemon.py # (c) 2017-2018 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 PyQt5.QtCore import QThread, QTimer, pyqtSignal, QObject, QMetaObject, Qt, pyqtSlot from PyQt5 import QtGui, QtWidgets from dbus.mainloop.pyqt5 import DBusQtMainLoop import dbus import time import logging as log logger = log.getLogger('monitorDaemon') logger.setLevel(log.DEBUG) path = os.path.abspath(os.path.expanduser('~/monitorDaemon.log')) fh = log.FileHandler(path) fh.setLevel(log.DEBUG) ch = log.StreamHandler() ch.setLevel(log.ERROR) formatter = log.Formatter("%(asctime)s %(levelname)s: %(message)s") fh.setFormatter(formatter) ch.setFormatter(formatter) logger.addHandler(fh) logger.addHandler(ch) DEFAULT_TIMEOUT = 500 GRACE_TIMEOUT = 3000 VERBOSE = True class xrandrInterface(QObject): """ Provides an interface to the python implementation of xrandr. It does all the processing used to enable/disable monitors etc. Inhertis from QThread. """ screenConfigChanged = pyqtSignal() finished = pyqtSignal() def __init__(self): """ Initialize the module. """ - QThread.__init__(self) + super(xrandrInterface, self).__init__() self.display = display.Display() self.screen = self.display.screen() self.running = True self.processing = 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.crtcStatus = {} self.getScreenInformation() self.enableEvents() self.timer = QTimer() self.timer.setSingleShot(True) + + self.xrThread = QThread() + self.xrThreadObj = xrThreadObject(self.display) + + @pyqtSlot() + def startThreads(self): + logger.debug("startThreads") + self.xrThreadObj.moveToThread(self.xrThread) + + self.xrThread.started.connect(self.xrThreadObj.run) + self.xrThread.finished.connect(QtWidgets.qApp.exit) + self.xrThreadObj.finished.connect(self.xrThread.quit) + self.xrThreadObj.screenChange.connect(self.screenChange) + self.timer.timeout.connect(self.screenInformationChanged) + self.xrThread.start() def resumeWork(self): """ Resume processing """ self.processing = True def suspendWork(self): """ Suspend processing """ self.processing = False self.timer.stop() @pyqtSlot() + def screenChange(self): + logger.debug("screenChange") + self.timer.stop() + self.timer.start(DEFAULT_TIMEOUT) + + @pyqtSlot() def screenInformationChanged(self): """ Is called when the daemon detects a change in the screen configuration. :return: Nothing """ logger.debug("screenInformationChanged") + self.getScreenInformation() self.screenConfigChanged.emit() def disableEvents(self): """ Disables event processing in the xrandr interface. You can call it to temporarily switch events off. :return: Nothing """ logger.debug("disableEvents") self.window.xrandr_select_input(0) def enableEvents(self): """ Enable event processing in the xrandr interface. You need to call it to switch events back on after calling :func:`disableEvents` :return: Nothing """ logger.debug("enableEvents") self.window.xrandr_select_input( randr.RRScreenChangeNotifyMask #| randr.RRCrtcChangeNotifyMask #| randr.RROutputChangeNotifyMask #| randr.RROutputPropertyNotifyMask ) def getScreenInformation(self): """ Retrieves the current screen information from the XRandR extension and stores it at self.outputStates as well as self.crtcStatus. It takes a while to process, be sure to not call it too often. :return: Nothing """ logger.debug("getScreenInformation") 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): """ Apply a configuration that has been previously stored. :param config: The configuration to apply :return: Nothing """ logger.debug("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) # 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() logger.debug("loadAndApply done.") def updateScreenSize(self): """ Calculate the required screen size based on the connectec CRTC sizes and update the virtual screen size accordingly. :return: Nothing """ logger.debug("updateScreenSize") # 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 = int((width / 96.0) * 25.4 + 0.5); mm_height = int((height / 96.0) * 25.4 + 0.5); self.window.xrandr_set_screen_size(width, height, mm_width, mm_height) def getScreenConfiguration(self): """ Retrieve the current screen configuration. This is not live, i.e. it simply returns the stored screen configuration based on hotplug events. :return: The output status """ logger.debug("getScreenConfiguration") return self.outputStatus def disableCrtc(self, crtc): """ Disable a CRTC. :param crtc: The number of the CRTC to disable. :return: Nothing """ logger.debug("disableCrtc") 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): """ Retrieve the CRTC for a given output. :param output: The output to work on :return: The connected CRTC """ logger.debug("getCurrentCrtcForOutput") 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): """ Configures a CRTC to a given output and a given mode. :param crtc: The CRTC to configure :param output: The output to configure :param mode: The new mode to set :param posx: The X position of the screen :param posy: The Y position of the screen :return: Nothing """ logger.debug("configureCrtcForOutputAndMode") 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): """ Retrieve the output number by output name :param name: The output name :return: The output number """ logger.debug("getOutputForOutputName") 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): """ Return the first CRTC an output can be connected to. :param output: The output to retrieve information from :return: The CRTC number or None """ logger.debug("getUsableCrtcForOuput") 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): """ Find the internal mode number associated with a given mode name. :param output: The output to work with :param modename: The modename to retrieve :return: The mode number or None """ logger.debug("findModeByName") 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): """ Parse the mode names returned by an output into name and data dict :param mode_names: The mode names to extract :param modes: The raw mode data :return: A dict with matched up mode data and mode names """ logger.debug("parseModes") 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): """ Return the output a screen with a given EDID is connected to. :param edid: The EDID to look for :return: The output number or None """ logger.debug("getOuputForEdid") 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): """ Retrieve the EDID for a given output :param output_nr: The output to look for :return: The hexdigest of the EDID or None """ logger.debug("get_edid") PROPERTY_EDID = self.display.intern_atom("EDID", 0) INT_TYPE = 19 try: props = self.display.xrandr_list_output_properties(output_nr) except: logger.error("Exception gettint output properties") return None if PROPERTY_EDID in props.atoms: try: rawedid = self.display.xrandr_get_output_property(output_nr, PROPERTY_EDID, INT_TYPE, 0, 400) except: logger.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 - + +class xrThreadObject(QObject): + + screenChange = pyqtSignal() + finished = pyqtSignal() + + def __init__(self, display): + super(xrThreadObject, self).__init__() + self.display = display + self.running = True + def run(self): """ The main loop of the thread. It constantly pools the xrandr interface for new events, since doing blocked I/O sometimes locks up the interface. New events are processed on demand, especially, the change of screen information. A grace timer protects the events to be processed several times. """ + while self.running: ev = self.display.pending_events() if(ev == 0): time.sleep(0.01) continue e = self.display.next_event() logger.debug("Got Event: " + str(e._data)) # 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: logger.debug("ScreenChangeNotify") - # print(e._data) - if self.processing: - logger.info("Starting Timer...") - self.timer.stop() - self.timer.start(DEFAULT_TIMEOUT) - else: - logger.debug("Processing is inhibited") + self.screenChange.emit() # 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: # logger.debug('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) logger.debug("Emit finished") self.finished.emit() class ConfigManager: """ Provides an interface to the configuration file """ def __init__(self, cfgfile): """ Initialize the module """ self.cfgfile = cfgfile if not os.path.exists(cfgfile): self.recreateCfg() self.dic = {} self.cfg2dic() def getDic(self): """ Retrieve the current configuration :return: The dictionary """ return self.dic def recreateCfg(self): """ Create a new configuration file, overwriting an existing file. :return: Nothing """ logger.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): """ Convert the configuration file to a dictionary. :return: The parsed dictionary """ 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): """ Convert the dictionary to the configuration file and save it. :return: Nothing """ 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): """ Retrieve the stored configuration mathing a given screen configuration. :return: A tuple with the configuration and the name or (None, None) """ 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): """ Check if autosave is enabled. :return: bool """ if self.dic['General']['autosave'] == '1': return True return False def loadOnStartupEnabled(self): """ Check if load on startup is enabled. :return: bool """ if self.dic['General']['loadonstartup'] == '1': return True return False def getApplicationConfig(self): """ Return the application configuration dictionary, i.e. the "General" section. :return: The dictionary configuration """ return self.dic['General'] def saveApplicationConfig(self, cfg): """ Save the application configuration, i.e. the "General" section. :param cfg: The dictionary with the configuration to save :return: Nothing """ ne = self.dic['General'] for key in cfg: self.dic['General'][key] = cfg[key] self.dic2cfg() def saveConfiguration(self, screenConfiguration): """ Save a given screen configuration to the configuratoin file, overwriting an existing identical configuration. :param screenConfiguration: The screen configuration to save :return: bool """ logger.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(QtWidgets.QWidget): """ This module contains the graphical user interface for the user-configurable settings. """ loadSettings = pyqtSignal() saveSettings = pyqtSignal([dict]) quitApp = pyqtSignal() def __init__(self, parent=None): """ Initialize the module. """ self.parent = parent QtWidgets.QWidget.__init__(self, parent) self.setWindowTitle('monitorDaemon Settings GUI') grid = QtWidgets.QGridLayout() self.setLayout(grid) lbl1 = QtWidgets.QLabel('Load on Startup') lbl2 = QtWidgets.QLabel('AutoSave') self.cbAutoSave = QtWidgets.QCheckBox() self.cbLoadOnStartup = QtWidgets.QCheckBox() btn1 = QtWidgets.QPushButton('Save') btn2 = QtWidgets.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): """ Slot that is called when the application should be quit. :return: Nothing """ #QtWidgets.qApp.quit() self.quitApp.emit() @pyqtSlot() def saveBtnClicked(self): """ Slot that is invoked when the save button has been clicked. :return: Nothing """ 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): """ Toggle the visibility of the settings manager. :return: Nothing """ if reason != QtWidgets.QSystemTrayIcon.Trigger: return if self.isVisible(): self.hide() else: self.loadSettings.emit() self.show() @pyqtSlot(dict) def settingsLoaded(self, settings): """ Slot that is called by the when the settings GUI should be initialized with values. :return: Nothing """ 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(QtWidgets.QSystemTrayIcon): """ Provides the system tray icon. """ def __init__(self, icon, m, parent=None): """ Initialize the module and connect signals/slots. """ self.parent = parent QtWidgets.QSystemTrayIcon.__init__(self, icon, parent) menu = QtWidgets.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.quitApp) self.setContextMenu(menu) self.settingsGui = SettingsGui() self.activated.connect(self.settingsGui.toggleVisibility) self.settingsGui.loadSettings.connect(m.loadSettings) self.settingsGui.saveSettings.connect(m.saveSettings) self.settingsGui.quitApp.connect(m.quitApp) m.settingsLoaded.connect(self.settingsGui.settingsLoaded) class Manager(QObject): """ Handles interaction between SystemTrayIcon, Settings and XRandR interface. """ applyConfig = pyqtSignal([dict]) settingsLoaded = pyqtSignal([dict]) def __init__(self, config, xri, parent = None): """ Initialize the module. """ QObject.__init__(self) self.pendingConfigChange = False self.config = config self.parent = parent self.xri = xri self.previousConfig = {} self.xdgSessionId = os.getenv('XDG_SESSION_ID') self.systemBus = None self.initDbus() def unlockHandler(self): """ Handle unlocking the session. Delay processing until the session is active again. """ logger.debug("unlockHandler") if self.pendingConfigChange: logger.debug("Waiting up to five seconds for the session to be active again...") count = 0 timeout = False while count < 5: if isSessionActive(): logger.debug("Running pending screenConfigurationChanged") self.pendingConfigChange = False self.screenConfigurationChanged() return else: logger.debug("Sleeping") count += 1 time.sleep(1) logger.debug("Timeout in unlockHandler") def suspendHandler(self, sleeping): """ Handle system suspend. If system is going to sleep, suspend any work. If we are resuming, resume work operation """ if sleeping: logger.debug('System going to sleep') self.xri.suspendWork() else: logger.debug('System resuming') self.xri.resumeWork() def initDbus(self): """ Get on the system bus to register for session lock/unlock requests """ dbus_loop = DBusQtMainLoop(set_as_default=True) self.systemBus = dbus.SystemBus(mainloop = dbus_loop) sessionInterface = 'org.freedesktop.login1.Session' managerInterface = 'org.freedesktop.login1.Manager' self.systemBus.add_signal_receiver(self.unlockHandler, 'Unlock', sessionInterface) self.systemBus.add_signal_receiver(self.suspendHandler, 'PrepareForSleep', managerInterface) def isSessionActive(self): """ Checks whether the session is active. :return: True if active, otherwise False """ serviceName = 'org.freedesktop.login1' servicePath = '/org/freedesktop/login1/session/' + self.xdgSessionId interface = 'org.freedesktop.login1.Session' sessionObj = self.systemBus.get_object(serviceName, servicePath) props_iface = dbus.Interface(sessionObj, 'org.freedesktop.DBus.Properties') props = props_iface.GetAll(interface) return props['State'] == "active" @pyqtSlot() def saveConfig(self): """ Slot that is called when the user clicks the "Save Settings" button. :return: Nothing """ screenConfig = self.xri.getScreenConfiguration() self.config.saveConfiguration(screenConfig) @pyqtSlot() def loadAndApply(self): """ Slot that is called when the user clicks the "Load and Apply" button. :return: Nothing """ logger.info("loadAndApply") screenConfig = self.xri.getScreenConfiguration() storedConfig, name = self.config.getConfig(screenConfig) if storedConfig: self.applyConfig.emit(storedConfig) self.previousConfig = storedConfig + else: + logger.info('No stored configuration found') @pyqtSlot(dict) def saveSettings(self, cfg): """ Slot that is called by the settings GUI to save application settings. :return: Nothing """ self.config.saveApplicationConfig(cfg) @pyqtSlot() def loadSettings(self): """ Slot that is called by the settings GUI to load the application settings. :return: Nothing """ cfg = self.config.getApplicationConfig() self.settingsLoaded.emit(cfg) @pyqtSlot() def loadOnStartup(self): """ Slot that is called when the application starts up. It loads the current configuration if it matches and auto-load on startup is enabled. :return: Nothing """ logger.info("loadOnStartup") if self.config.loadOnStartupEnabled(): self.screenConfigurationChanged() @pyqtSlot() def quitApp(self): logger.debug("Stopping Thread...") - self.xri.running = False + self.xri.xrThreadObj.running = False def comparePreviousConfig(self, actConfig, previousConfig): """ Compare actual config with previous config. :return: True if both configs match, otherwise False """ logger.debug("Comparing previous configuration") logger.debug(actConfig) logger.debug(previousConfig) numOutputs = len(actConfig.keys()) numConfigs = len(previousConfig.keys()) / 5 if numOutputs != numConfigs: logger.debug("Different number of outputs") return False for output in actConfig: edid = actConfig[output]['edid'] lFound = False for ii in range(1, numConfigs+1): if previousConfig.has_key("edid"+str(ii)): if previousConfig["edid"+str(ii)] == edid: lFound = True if not lFound: logger.debug("EDID not found in previous config: %s", edid) return False logger.debug("Outputs match") return True def compareConfig(self, actConfig, storedConfig): """ Function that checks whether two configurations are the same with regard to the screen configuration. The CRTC configuration does not matter here. :return: True if configurations match, otherwise False """ logger.debug("Comparing configurations") numOutputs = len(actConfig.keys()) for output in actConfig: for ii in range(1, numOutputs+1): if actConfig[output]['edid'] == storedConfig['edid'+str(ii)]: if actConfig[output]['modename'] != storedConfig['mode'+str(ii)]: logger.debug('Modename different: %s vs %s', actConfig[output]['modename'], storedConfig['mode'+str(ii)]) return False if actConfig[output]['posx'] != int(storedConfig['posx'+str(ii)]): logger.debug('PosX different: %s vs %s', actConfig[output]['posx'], storedConfig['posx'+str(ii)]) return False if actConfig[output]['posy'] != int(storedConfig['posy'+str(ii)]): logger.debug('PosY different: %s vs %s', actConfig[output]['posy'], storedConfig['posy'+str(ii)]) return False logger.debug("Configurations match") return True @pyqtSlot() def screenConfigurationChanged(self): """ Slot that is called by the XRandR interface when the screen information changes. This is probably one of the most important methods since it triggers the retrieval of the stored configuration and applies the configuration if necessary. :return: Nothing """ logger.info("screenConfigurationChanged") if not self.isSessionActive(): logger.debug("Deferring screen config change since session is not active") self.pendingConfigChange = True return actConfig = self.xri.getScreenConfiguration() if self.comparePreviousConfig(actConfig, self.previousConfig): logger.debug('No new connection, triggering save if necessary') if self.config.autosaveEnabled(): self.saveConfig() return storedConfig, name = self.config.getConfig(actConfig) if storedConfig and not self.compareConfig(actConfig, storedConfig): self.applyConfig.emit(storedConfig) self.previousConfig = storedConfig elif storedConfig: self.previousConfig = storedConfig #@pyqtSlot() #def quit(self): # """ Slot that is called when the aplication is to quit. """ # QtWidgets.qApp.quit() def main(): """ Main application startup. Initialize the configuration file, the GUI and load applicatoin settings. Fire up the XRandR interface and listen for events. There are no shared queues or objects, since all communication is done via Qt signals/slots. """ app = QtWidgets.QApplication(sys.argv) app.setQuitOnLastWindowClosed(False); settingsdir = os.path.join(os.environ['HOME'], '.config') cfgfile = os.path.join(settingsdir, 'monitorDaemon.ini') cm = ConfigManager(cfgfile) - objThread = QThread() - w = QtWidgets.QWidget() x = xrandrInterface() - x.moveToThread(objThread) - objThread.started.connect(x.run) - objThread.finished.connect(app.exit) m = Manager(cm, x, w) x.screenConfigChanged.connect(m.screenConfigurationChanged) - x.finished.connect(objThread.quit) m.applyConfig.connect(x.applyConfiguration) - QMetaObject.invokeMethod(objThread, "start", Qt.QueuedConnection) + QMetaObject.invokeMethod(x, "startThreads", 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 QtWidgets.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__': """ Call the main method if we run standalone. """ main()