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)
예제 #2
0
				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]),
						})
예제 #3
0
	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', '') )) )
예제 #4
0
	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.)'''
				)
예제 #5
0
 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'))
예제 #6
0
		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)
예제 #7
0
	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')
예제 #8
0
	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 )
예제 #9
0
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))
예제 #10
0
	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
예제 #11
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) )
예제 #12
0
	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()))
예제 #13
0
	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) )