def __init__(self, parent=None, showHitRects=False): super().__init__(parent) # API init. self._customStyleSheet = self.styleSheet() #always '' for some reason if showHitRects: self.setStyleSheet(f""" background: rgba(0,0,0,128); border: 4px solid black; """) self.zoomLabel = QLabel(self) def setZoomLabelStyle(name): theme_ = theme(name) self.zoomLabel.setStyleSheet(f""" color: {theme_.text}; background-color: {theme_.interactiveVideoArea.chickletBackground}; border: 1px solid {theme_.border}; margin: 10px 5px; padding: 5px 10px; border-radius: 17px; """) settings.observe('theme', 'dark', setZoomLabelStyle) self.zoomLabel.setText(self.zoomLabelTemplate.format(zoom=2)) self.zoomLabel.show() if showHitRects: #Make black background show up in Designer. Must be async for some reason. delay(self, 0, lambda: self.setAutoFillBackground(True)) if api: self.lastClickTime = 0 api.observe('videoZoom', self.updateVideoZoom) self.grabGesture(Qt.PinchGesture)
def checkDf(*, timeout): exitStatus = df.poll() if exitStatus is None: #Still running, check again later. #Standard clamped exponential decay. Keeps polling to a reasonable amount, frequent at first then low. delay(self, timeout, lambda: checkDf(timeout=max(1, timeout*2)) ) elif exitStatus: #df failure, raise an error if exitStatus == 1: #When a storage device with multiple partitions is removed, #the observer fires once for each partition. This means #that, for one partition, the client will issue a spurious #call to this function with the stale partition's device. log.debug(f'Unknown device {device}.') log.debug(f'Known devices are {[p["device"] for p in self._partitions]}.') else: raise Exception(f'df exited with error {exitStatus}') else: info = ( #Chop up df command output. df.communicate()[0] .split(b'\n')[1] #Remove first line, column headings .split() #Each output is now in a list. ) callback({ 'available': int(info[0]), 'used': int(info[1]), 'total': int(info[0]) + int(info[1]), })
def onShow(self): #Try, _again_, to set the drop-down to the correct value. Since this widget is #repopulated when the partitions change and on show, this is really hard. >_< api.externalPartitions.observe(lambda *_: self.setPreferredSavingDevice( settings.value('preferredFileSavingUUID', '') )) delay(self, 16, lambda: api.externalPartitions.observe(lambda *_: self.setPreferredSavingDevice( settings.value('preferredFileSavingUUID', '') )) )
def _asyncCallFinished(self, watcher): log.debug(f'finished async call: {self}') self.performance['finished'] = perf_counter() self._done = True reply = QDBusPendingReply(watcher) try: if reply.isError(): if self._catches: error = reply.error() for catch in self._catches: try: error = catch(error) except Exception as e: error = e else: #This won't do much, but (I'm assuming) most calls simply won't ever fail. if reply.error().name() == 'org.freedesktop.DBus.Error.NoReply': raise DBusException(f"{self} timed out ({API_TIMEOUT_MS}ms)") else: raise DBusException("%s: %s" % (reply.error().name(), reply.error().message())) else: value = reply.value() for then in self._thens: try: value = then(value) except Exception as error: if self._catches: for catch in self._catches: try: error = catch(error) except Exception as e: error = e else: raise e except Exception as e: raise e finally: #Wait a little while before starting on the next callback. #This makes the UI run much smoother, and usually the lag #is covered by the UI updating another few times anyway. #Note that because each call still lags a little, this #causes a few dropped frames every time the API is called. delay(self, API_INTERCALL_DELAY, self.api._startNextCallback) self.performance['handled'] = perf_counter() if self.performance['finished'] - self.performance['started'] > API_SLOW_WARN_MS / 1000: log.warn( f'''slow call: {self} took { (self.performance['finished'] - self.performance['started'])*1000 :0.0f}ms/{API_SLOW_WARN_MS}ms. (Total call time was { (self.performance['handled'] - self.performance['enqueued'])*1000 :0.0f}ms.)''' )
def checkProc(*, timeout): exitStatus = proc.poll() if exitStatus is None: #Still running, check again later. #Standard clamped exponential decay. Keeps polling to a reasonable amount, frequent at first then low. delay(self, timeout, lambda: checkProc(timeout=max(250, timeout * 2))) elif exitStatus: error(exitStatus) else: converter = (lambda x, y: x) if binaryOutput else str success(converter(proc.communicate()[0], 'utf8'))
def checkProc(*, timeout): nonlocal currentLine, currentEntry try: poll = proc.poll() data = proc.stdout.read() #this read kills the process: #data = os.read(proc.stdout.fileno(), 1024) #read() seems a bit more stable, but still not 100%. I don't have any idea why. --DDR 2019-10-29 if data: data = data.decode('utf8') if data: lines = data.split('\n') #Load the remainder of the current line. currentLine += lines[0] currentEntry.setData(currentLine, Qt.EditRole) #If any new line(s), append them. If they are unfinished, we'll load the remainder of them next time around. for line in lines[1:]: currentLine = line currentEntry = QStandardItem(line) script['output'].appendRow(currentEntry) lines[1:2] and self.scrollOutputToBottom(script) delay(self, timeout, lambda: checkProc(timeout=16) ) elif poll is None: #Subprocess hasn't exited yet. delay(self, timeout, lambda: checkProc(timeout=max(250, timeout*2)) ) else: message = f'exit {proc.poll()}' if currentLine: currentEntry = QStandardItem(message) script['output'].appendRow(currentEntry) else: currentEntry.setData(message, Qt.EditRole) self.scriptStopped(index) self.scrollOutputToBottom(script) except OSError: message = f'process output closed' #Exit code is None, the process is still running, we just can't read from it. if currentLine: currentEntry = QStandardItem(message) script['output'].appendRow(currentEntry) else: currentEntry.setData(message, Qt.EditRole) self.scriptStopped(index) self.scrollOutputToBottom(script)
def __init__(self, parent=None, showHitRects=False): super().__init__(parent) def initialiseStyleSheet(): self.baseStyleSheet = self.styleSheet() settings.observe('theme', 'dark', lambda name: self.setStyleSheet(f""" font-size: 16px; background: transparent; color: {theme(name).text}; """ + self.baseStyleSheet ) ) delay(self, 0, initialiseStyleSheet) #Delay until after init is done and stylesheet is set. NFI why this isn't handled by super().__init__. # Set some default text, so we can see the widget. if not self.text(): self.setText('text')
def __init__(self, parent=None, showHitRects=False): self.keepActiveLook = False super().__init__(parent, showHitRects=showHitRects) self.hintList = [] # Set some default text, so we can see the widget. if not self.text(): self.setText('text input') self.setCursorMoveStyle(Qt.LogicalMoveStyle) #Left moves left, right moves right. Defaults is right arrow key moves left under rtl writing systems. self.theme = theme('dark') self.clickMarginColor = f"rgba({randint(128, 255)}, {randint(64, 128)}, {randint(0, 32)}, {randint(32,96)})" settings.observe('theme', 'dark', lambda name: ( setattr(self, 'theme', theme(name)), self.refreshStyle(), )) if self.isClearButtonEnabled(): clearButton = self.findChild(QToolButton) clearButtonGeom = clearButton.geometry() clearButtonGeom.moveLeft(clearButtonGeom.left() - self.touchMargins['left']) clearButton.setGeometry(clearButtonGeom) self.inputMode = '' #Set to empty, 'jogWheel', or 'touch'. Used for defocus event handling behaviour. self.jogWheelLowResolutionRotation.connect(self.handleJogWheelRotation) self.jogWheelClick.connect(self.jogWheelClicked) self.touchEnd.connect(self.editTapped) self.doneEditing.connect(self.doneEditingCallback) #When we tap an input, we deselect selected text. But we want to #select all text. So, select it again after we've tapped it. #Note: This only applies if the keyboard hasn't bumped the text #out of the way first. self.selectAllTimer = QTimer() self.selectAllTimer.timeout.connect(self.selectAll) self.selectAllTimer.setSingleShot(True) delay(self, 0, #Must be delayed until after creation for isClearButtonEnabled to be set. self.__hackMoveClearButtonToCompensateForIncreasedMargin )
def run(self, command: list, error: callable, success: callable, *, binaryOutput=False): """Run the command, passing stdout to success. command: a list of [command, ...args] success: Called with stdout on a zero exit-status. error: Called with the exit status if 1-255. Note: This function exists because Python 3.5 grew equivalent, but we don't have access to it yet here in 3.4. Note: Non-static method, because something must own the QTimer behind delay().""" assert command and error and success proc = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ) def checkProc(*, timeout): exitStatus = proc.poll() if exitStatus is None: #Still running, check again later. #Standard clamped exponential decay. Keeps polling to a reasonable amount, frequent at first then low. delay(self, timeout, lambda: checkProc(timeout=max(250, timeout * 2))) elif exitStatus: error(exitStatus) else: converter = (lambda x, y: x) if binaryOutput else str success(converter(proc.communicate()[0], 'utf8')) delay( self, 200, lambda: #Initial delay, df et al usually run in .17-.20s. checkProc(timeout=50))
def selectMarkedRegion(self, pos: QtCore.QModelIndex): if self.lastSelectedRegion == pos.row(): return else: self.lastSelectedRegion = pos.row() def assign(self, index, status): """Hack to work around not being able to assign in a lambda.""" self.markedRegions[index]['highlight'] = status self.uiMarkedRegionVisualization.update() def assignCatpureIndex(self, index, status): """Hack to capture the value of index in a closure. Index which will otherwise get changed by the time we use it, if it is used in a lambda.""" return lambda: assign(self, index, status) for index in range(len(self.markedRegions)): if index == pos.row(): duration = 400 self.markedRegions[index]['highlight'] = -1 delay(self, 1/3 * duration, assignCatpureIndex(self, index, 1)) delay(self, 2/3 * duration, assignCatpureIndex(self, index, -1)) delay(self, 3/3 * duration, assignCatpureIndex(self, index, 1)) else: self.markedRegions[index]['highlight'] = 0
def usageFor(self, device: str, callback: Callable[[], Dict[str,int]]): for partition in self._partitions: if partition['device'] == device: df = subprocess.Popen( ['df', partition['path'], '--output=avail,used'], #used+avail != 1k-blocks stdout=subprocess.PIPE, stderr=subprocess.DEVNULL ) def checkDf(*, timeout): exitStatus = df.poll() if exitStatus is None: #Still running, check again later. #Standard clamped exponential decay. Keeps polling to a reasonable amount, frequent at first then low. delay(self, timeout, lambda: checkDf(timeout=max(1, timeout*2)) ) elif exitStatus: #df failure, raise an error if exitStatus == 1: #When a storage device with multiple partitions is removed, #the observer fires once for each partition. This means #that, for one partition, the client will issue a spurious #call to this function with the stale partition's device. log.debug(f'Unknown device {device}.') log.debug(f'Known devices are {[p["device"] for p in self._partitions]}.') else: raise Exception(f'df exited with error {exitStatus}') else: info = ( #Chop up df command output. df.communicate()[0] .split(b'\n')[1] #Remove first line, column headings .split() #Each output is now in a list. ) callback({ 'available': int(info[0]), 'used': int(info[1]), 'total': int(info[0]) + int(info[1]), }) delay(self, 0.20, lambda: #Initial delay, df usually runs in .17s. checkDf(timeout=0.05) )
def blinkBatteryAFewTimes(self): delay(self, 750*1, lambda: (setattr(self, '_batteryBlink', True), self.updateBatteryIcon())) delay(self, 750*2, lambda: (setattr(self, '_batteryBlink', False), self.updateBatteryIcon())) delay(self, 750*3, lambda: (setattr(self, '_batteryBlink', True), self.updateBatteryIcon())) delay(self, 750*4, lambda: (setattr(self, '_batteryBlink', False), self.updateBatteryIcon())) delay(self, 750*5, lambda: (setattr(self, '_batteryBlink', True), self.updateBatteryIcon())) delay(self, 750*6, lambda: (setattr(self, '_batteryBlink', False), self.updateBatteryIcon())) delay(self, 750*7, lambda: (setattr(self, '_batteryBlink', True), self.updateBatteryIcon())) delay(self, 750*8, lambda: (setattr(self, '_batteryBlink', False), self.updateBatteryIcon()))
def executeAndPoll(self, index: QtCore.QModelIndex): """Run the user script. See http://eyalarubas.com/python-subproc-nonblock.html for commentary and additional approaches.""" script = index.data(Qt.UserRole) proc = subprocess.Popen( script['path'], stdout=subprocess.PIPE, stderr=sys.stdout, #Echo stderr to our logs, so they can be retrieved and watched for debugging. shell=True, cwd=self.path, env=dict({('GUI_PID', str(os.getpid()))} | os.environ.items()) #Use this for SIGSTOP and SIGCONT, NOT SIGKILL. Run service stop chronos-gui2[-dev] for that. Note that $PPID is the parent *shell* we spawn, not the gui, which is why we provide the gui variable. ) script['process'] = proc self.scripts.setData(index, script, Qt.UserRole ) flags = fcntl(proc.stdout, F_GETFL) fcntl(proc.stdout, F_SETFL, flags | os.O_NONBLOCK) currentLine = '' currentEntry = QStandardItem(currentLine) script['output'].clear() script['output'].appendRow(currentEntry) def checkProc(*, timeout): nonlocal currentLine, currentEntry try: poll = proc.poll() data = proc.stdout.read() #this read kills the process: #data = os.read(proc.stdout.fileno(), 1024) #read() seems a bit more stable, but still not 100%. I don't have any idea why. --DDR 2019-10-29 if data: data = data.decode('utf8') if data: lines = data.split('\n') #Load the remainder of the current line. currentLine += lines[0] currentEntry.setData(currentLine, Qt.EditRole) #If any new line(s), append them. If they are unfinished, we'll load the remainder of them next time around. for line in lines[1:]: currentLine = line currentEntry = QStandardItem(line) script['output'].appendRow(currentEntry) lines[1:2] and self.scrollOutputToBottom(script) delay(self, timeout, lambda: checkProc(timeout=16) ) elif poll is None: #Subprocess hasn't exited yet. delay(self, timeout, lambda: checkProc(timeout=max(250, timeout*2)) ) else: message = f'exit {proc.poll()}' if currentLine: currentEntry = QStandardItem(message) script['output'].appendRow(currentEntry) else: currentEntry.setData(message, Qt.EditRole) self.scriptStopped(index) self.scrollOutputToBottom(script) except OSError: message = f'process output closed' #Exit code is None, the process is still running, we just can't read from it. if currentLine: currentEntry = QStandardItem(message) script['output'].appendRow(currentEntry) else: currentEntry.setData(message, Qt.EditRole) self.scriptStopped(index) self.scrollOutputToBottom(script) delay(self, 200, lambda: #Initial delay for startup, then try every frame at most or 4x a second at least. Hopefully we get more than 4fps. 😬 checkProc(timeout=16) )