class main(QWidget): def __init__(self, parent=None): super(main, self).__init__(parent) self.setup() # connections, widgets, layouts, etc. self.blksize = 2**20 # 1 MB; must be divisible by 16 self.ext = '.enc' # extension is appended to encrypted files self.path = '' self.encrypted = [] # to highlight done files in list self.decrypted = [] self.clipboard = QApplication.clipboard() self.timeout = None # to clear message label, see setMessage # this program was just an excuse to play with QprogressBar if not hash(os.urandom(11)) % 11: QTimer().singleShot(50, self.windDown) # various random hints hints = [ 'Freshly encrypted files can be renamed in the table!', 'Clipboard is always cleared on program close!', 'Keys can contain emoji if you <em>really</em> want: \U0001f4e6', 'Keys can contain emoji if you <em>really</em> want: \U0001F511', 'This isn\'t a tip, I just wanted to say hello!', 'Keys can be anywhere from 8 to 4096 characters long!', 'This program was just an excuse to play with the progress bars!', 'Select \'Party\' in the hash button for progress bar fun!', ('Did you know you can donate one or all of your vital organs to ' 'the Aperture Science Self-Esteem Fund for Girls? It\'s true!'), ('It\'s been {:,} days since Half-Life 2: Episode ' 'Two'.format(int((time.time() - 1191988800) / 86400))), 'I\'m version {}!'.format(VERSION), 'I\'m version {}.whatever!'.format(VERSION.split('.')[0]), ('Brought to you by me, I\'m <a href="https://orthallelous.word' 'press.com/">Orthallelous!</a>'), #'Brought to you by me, I\'m Htom Sirveaux!', 'I wonder if there\'s beer on the sun', 'Raspberry World: For all your raspberry needs. Off the beltline', #'I\'ve plummented to my death and I can\'t get up', '<em>NOT</em> compatible with the older version!', ('Hello there, fellow space travellers! Until somebody gives me ' 'some new lines in KAS, that is all I can say. - Bentusi Exchange' ) ] if not hash(os.urandom(9)) % 4: self.extraLabel.setText(random.choice(hints)) def genKey(self): "generate a random key" n = self.keySizeSB.value() char = string.printable.rstrip() #map(chr, range(256)) while len(char) < n: char += char key = ''.join(random.sample(char, n)) self.keyInput.setText(key) def showKey(self, state=None): "hide/show key characters" if state is None: state = bool(self.showKeyCB.checkState()) else: state = bool(state) if state: self.keyInput.setEchoMode(QLineEdit.Normal) else: self.keyInput.setEchoMode(QLineEdit.PasswordEchoOnEdit) def getFolder(self): "open file dialog and fill file table" path = QFileDialog(directory=self.path).getExistingDirectory() if not path: return self.path = str(path) self.populateTable(self.path) self.encrypted, self.decrypted = [], [] return def resizeEvent(self, event): self.showFolder(self.path) # update how the folder is shown def splitterChanged(self, pos): self.showFolder(self.path) # likewise def showFolder(self, path): "displays current path, truncating as needed" if not path: return ell, sl = '\u2026', os.path.sep # ellipsis, slash chars lfg, rfg = Qt.ElideLeft, Qt.ElideRight lst, wdh = os.path.basename(path), self.folderLabel.width() path = path.replace(os.path.altsep or '\\', sl) self.folderLabel.setToolTip(path) # truncate folder location fnt = QFontMetrics(self.folderLabel.font()) txt = str(fnt.elidedText(path, lfg, wdh)) if len(txt) <= 1: # label is way too short self.folderLabel.setText('\u22ee' if txt != sl else txt) return # but when would this happen? # truncate some more (don't show part of a folder name) if len(txt) < len(path) and txt[1] != sl: txt = ell + sl + txt.split(sl, 1)[-1] # don't truncate remaining folder name from the left if txt[2:] != lst and len(txt[2:]) < len(lst) + 2: txt = str(fnt.elidedText(ell + sl + lst, rfg, wdh)) # you'd think len(txt) < len(lst) would work, but no; you'd be wrong self.folderLabel.setText(txt) def populateTable(self, path): "fill file table with file names" self.showFolder(path) names = [] for n in os.listdir(path): if os.path.isdir(os.path.join(path, n)): continue # folder names.append(n) self.folderTable.clearContents() self.folderTable.setRowCount(len(names)) self.folderTable.setColumnCount(1) if not names: # no files in this folder, inform user self.setMessage('This folder has no files') return self.folderTable.blockSignals(True) selEnab = Qt.ItemIsSelectable | Qt.ItemIsEnabled for i, n in enumerate(names): item = QTableWidgetItem() item.setText(n) item.setToolTip(n) item.setFlags(selEnab) # color code encrypted/decrypted files if n in self.encrypted: item.setTextColor(QColor(211, 70, 0)) # allowed encrypted filenames to be changed item.setFlags(selEnab | Qt.ItemIsEditable) if n in self.decrypted: item.setForeground(QColor(0, 170, 255)) self.folderTable.setItem(i, 0, item) if len(names) > 5: self.setMessage('{:,} files'.format(len(names)), 7) self.folderTable.blockSignals(False) return def editFileName(self, item): "change file name" new, old = str(item.text()), str(item.toolTip()) result = QMessageBox.question( self, 'Renaming?', ("<p align='center'>Do you wish to rename<br>" + '<span style="color:#d34600;">{}</span>'.format(old) + "<br>to<br>" + '<span style="color:#ef4b00;">{}</span>'.format(new) + '<br>?</p>')) self.folderTable.blockSignals(True) if any(i in new for i in '/?<>:*|"^'): self.setMessage('Invalid character in name', 7) item.setText(old) elif result == QMessageBox.Yes: oold = os.path.join(self.path, old) try: os.rename(oold, os.path.join(self.path, new)) self.encrypted.remove(old) self.encrypted.append(new) item.setToolTip(new) except Exception as err: self.setMessage(str(err), 9) item.setText(old) item.setToolTip(old) self.encrypted.remove(new) self.encrypted.append(old) else: item.setText(old) self.folderTable.blockSignals(False) def setMessage(self, message, secs=4, col=None): "show a message for a few seconds - col must be rgb triplet tuple" if self.timeout: # https://stackoverflow.com/a/21081371 self.timeout.stop() self.timeout.deleteLater() if col is None: color = 'rgb(255, 170, 127)' else: try: color = 'rgb({}, {}, {})'.format(*col) except: color = 'rgb(255, 170, 127)' self.messageLabel.setStyleSheet('background-color: {};'.format(color)) self.messageLabel.setText(message) self.messageLabel.setToolTip(message) self.timeout = QTimer() self.timeout.timeout.connect(self.clearMessage) self.timeout.setSingleShot(True) self.timeout.start(secs * 1000) def clearMessage(self): self.messageLabel.setStyleSheet('') self.messageLabel.setToolTip('') self.messageLabel.setText('') def getName(self): "return file name of selected" items = self.folderTable.selectedItems() names = [str(i.text()) for i in items] if names: return names[0] # only the first selected file else: return '' def showKeyLen(self, string): "displays a tooltip showing length of key" s = len(string) note = '{:,} character{}'.format(s, '' if s == 1 else 's') tip = QToolTip pos = self.genKeyButton.mapToGlobal(QPoint(0, 0)) if s < self.minKeyLen: note = '<span style="color:#c80000;">{}</span>'.format(note) else: note = '<span style="color:#258f22;">{}</span>'.format(note) tip.showText(pos, note) def lock(self, flag=True): "locks buttons if True" stuff = [ self.openButton, self.encryptButton, self.decryptButton, self.genKeyButton, self.hashButton, self.showKeyCB, self.copyButton, self.keyInput, self.keySizeSB, self.folderTable, ] for i in stuff: i.blockSignals(flag) i.setEnabled(not flag) return def _lerp(self, v1, v2, numPts=10): "linearly interpolate from v1 to v2\nFrom Orthallelous" if len(v1) != len(v2): raise ValueError("different dimensions") D, V, n = [], [], abs(numPts) for i, u in enumerate(v1): D.append(v2[i] - u) for i in range(n + 1): vn = [] for j, u in enumerate(v1): vn.append(u + D[j] / float(n + 2) * i) V.append(tuple(vn)) return V def weeeeeee(self): "party time" self.lock() self.setMessage('Party time!', 2.5) a, b, c = self.encryptPbar, self.decryptPbar, self.hashPbar process, sleep = app.processEvents, time.sleep am, bm, cm = a.minimum(), b.minimum(), c.minimum() ax, bx, cx = a.maximum(), b.maximum(), c.maximum() a.reset() b.reset() c.reset() loops = self._lerp((am, bm, cm), (ax, bx, cx), 100) ivops = loops[::-1] # up and up! for i in range(3): for j, k, l in loops: a.setValue(int(j)) b.setValue(int(k)) c.setValue(int(l)) process() sleep(0.01) a.setValue(ax) b.setValue(bx) c.setValue(cx) sleep(0.25) a.setValue(am) b.setValue(bm) c.setValue(cm) # snake! self.setMessage('Snake time!') self.messageLabel.setStyleSheet('background-color: rgb(127,170,255);') for i in range(2): for j, k, l in loops: a.setValue(int(j)) process() sleep(0.002) process() a.setInvertedAppearance(True) process() for j, k, l in ivops: a.setValue(int(j)) process() sleep(0.002) for j, k, l in loops: b.setValue(int(k)) process() sleep(0.002) process() b.setInvertedAppearance(False) process() for j, k, l in ivops: b.setValue(int(k)) process() sleep(0.002) for j, k, l in loops: c.setValue(int(l)) process() sleep(0.002) process() c.setInvertedAppearance(True) process() for j, k, l in ivops: c.setValue(int(l)) process() sleep(0.002) process() b.setInvertedAppearance(True) process() for j, k, l in loops: b.setValue(int(k)) process() sleep(0.002) process() b.setInvertedAppearance(False) process() for j, k, l in ivops: b.setValue(int(k)) process() sleep(0.002) process() a.setInvertedAppearance(False) b.setInvertedAppearance(True) c.setInvertedAppearance(False) for j, k, l in loops: a.setValue(int(j)) process() sleep(0.002) process() a.setInvertedAppearance(True) process() for j, k, l in ivops: a.setValue(int(j)) process() sleep(0.002) # bars sleep(0.5) self.setMessage('Bars!') process() self.messageLabel.setStyleSheet('background-color: rgb(127,255,170);') for i in range(2): a.setValue(ax) time.sleep(0.65) a.setValue(am) sleep(0.25) process() b.setValue(bx) time.sleep(0.65) b.setValue(bm) sleep(0.25) process() c.setValue(cx) time.sleep(0.65) c.setValue(cm) sleep(0.25) process() b.setValue(bx) time.sleep(0.65) b.setValue(bm) sleep(0.25) process() # okay, enough process() a.setValue(ax) b.setValue(bx) c.setValue(cx) #a.setValue(am); b.setValue(bm); c.setValue(cm) a.setInvertedAppearance(False) b.setInvertedAppearance(True) c.setInvertedAppearance(False) self.lock(False) return def windDown(self, note=None): "silly deload on load" if note is None: note = 'Loading...' self.lock() self.setMessage(note) self.messageLabel.setStyleSheet('background-color: rgb(9, 190, 130);') a, b, c = self.encryptPbar, self.decryptPbar, self.hashPbar am, bm, cm = a.minimum(), b.minimum(), c.minimum() ax, bx, cx = a.maximum(), b.maximum(), c.maximum() a.reset() b.reset() c.reset() loops = self._lerp((ax, bx, cx), (am, bm, cm), 100) for j, k, l in loops: a.setValue(int(j)) b.setValue(int(k)) c.setValue(int(l)) app.processEvents() time.sleep(0.02) a.reset() b.reset() c.reset() self.lock(False) self.clearMessage() def genHash(self, action): "generate hash of selected file and display it" name, t0 = self.getName(), time.perf_counter() # mark what hash was used in the drop-down menu for i in self.hashButton.menu().actions(): if i == action: i.setIconVisibleInMenu(True) else: i.setIconVisibleInMenu(False) if str(action.text()) == 'Party': self.weeeeeee() self.windDown('Winding down...') return if not name: self.setMessage('No file selected') return if not os.path.exists(os.path.join(self.path, name)): self.setMessage('File does not exist') return self.lock() hsh = self.hashFile(os.path.join(self.path, name), getattr(hashlib, str(action.text()))) self.lock(False) #hsh = str(action.text()) + ': ' + hsh self.hashLabel.setText(hsh) self.hashLabel.setToolTip(hsh) self.extraLabel.setText( str(action.text()) + ' hash took ' + self.secs_fmt(time.perf_counter() - t0)) def setCancel(self): "cancel operation" self._requestStop = True def showCancelButton(self, state=False): "show/hide cancel button" self.cancelButton.blockSignals(not state) self.cancelButton.setEnabled(state) if state: self.cancelButton.show() self.keyInput.hide() self.genKeyButton.hide() self.keySizeSB.hide() else: self.cancelButton.hide() self.keyInput.show() self.genKeyButton.show() self.keySizeSB.show() def hashFile(self, fn, hasher): "returns the hash value of a file" hsh, blksize = hasher(), self.blksize fsz, csz = os.path.getsize(fn), 0.0 self.hashPbar.reset() self.showCancelButton(True) prog, title = '(# {:.02%}) {}', self.windowTitle() with open(fn, 'rb') as f: while 1: blk = f.read(blksize) if not blk: break hsh.update(blk) csz += blksize self.hashPbar.setValue(int(round(csz * 100.0 / fsz))) app.processEvents() self.setWindowTitle(prog.format(csz / fsz, title)) if self._requestStop: break self.hashPbar.setValue(self.hashPbar.maximum()) self.setWindowTitle(title) self.showCancelButton(False) if self._requestStop: self.setMessage('Hashing canceled!') self.hashPbar.setValue(self.hashPbar.minimum()) self._requestStop = False return return hsh.hexdigest() def hashKey(self, key, salt=b''): "hashes a key for encrypting/decrypting file" salt = salt.encode() if type(salt) != bytes else salt key = key.encode() if type(key) != bytes else key p = app.processEvents self.setMessage('Key Hashing...', col=(226, 182, 249)) p() key = hashlib.pbkdf2_hmac('sha512', key, salt, 444401) p() self.clearMessage() p() return hashlib.sha3_256(key).digest() # AES requires a 32 char key def encrypt(self): "encrypt selected file with key" name, t0 = self.getName(), time.perf_counter() if not name: self.setMessage('No file selected') return if not os.path.exists(os.path.join(self.path, name)): self.setMessage('File does not exist') return key = str(self.keyInput.text()) if len(key) < self.minKeyLen: self.setMessage(('Key must be at least ' '{} characters long').format(self.minKeyLen)) return self.lock() gn = self.encryptFile(key, os.path.join(self.path, name)) if not gn: self.lock(False) return self.encrypted.append(os.path.basename(gn)) self.lock(False) self.populateTable(self.path) # repopulate folder list bn, tt = os.path.basename(gn), time.perf_counter() - t0 self.setMessage('Encrypted, saved "{}"'.format(bn, 13)) self.extraLabel.setText('Encrypting took ' + self.secs_fmt(tt)) def encryptFile(self, key, fn): "encrypts a file using AES (MODE_GCM)" chars = ''.join(map(chr, range(256))).encode() chk = AES.block_size sample = random.sample iv = bytes(sample(chars, chk * 2)) salt = bytes(sample(chars * 2, 256)) vault = AES.new(self.hashKey(key, salt), AES.MODE_GCM, iv) fsz = os.path.getsize(fn) del key blksize = self.blksize gn = fn + self.ext fne = os.path.basename(fn).encode() fnz = len(fne) if len(fne) % chk: fne += bytes(sample(chars, chk - len(fne) % chk)) csz = 0.0 # current processed value self.encryptPbar.reset() prog, title = '({:.02%}) {}', self.windowTitle() self.showCancelButton(True) with open(fn, 'rb') as src, open(gn, 'wb') as dst: dst.write(bytes([0] * 16)) # spacer for MAC written at end dst.write(iv) dst.write(salt) # store iv, salt # is it safe to store MAC, iv, salt plain right in file? # can't really store them encrypted, # or elsewhere in this model of single file encryption? # can't have another file for the file to lug around # store file size, file name length dst.write(vault.encrypt(struct.pack('<2Q', fsz, fnz))) dst.write(vault.encrypt(fne)) # store filename while 1: dat = src.read(blksize) if not dat: break elif len(dat) % chk: # add padding fil = chk - len(dat) % chk dat += bytes(sample(chars, fil)) dst.write(vault.encrypt(dat)) csz += blksize # show progress self.encryptPbar.setValue(int(round(csz * 100.0 / fsz))) self.setWindowTitle(prog.format(csz / fsz, title)) app.processEvents() if self._requestStop: break if not self._requestStop: stuf = random.randrange(23) # pack in more stuffing fing = b''.join(bytes(sample(chars, 16)) for i in range(stuf)) dst.write(vault.encrypt(fing)) # and for annoyance dst.seek(0) dst.write(vault.digest()) # write MAC self.hashLabel.setText('MAC: ' + vault.hexdigest()) self.encryptPbar.setValue(self.encryptPbar.maximum()) self.setWindowTitle(title) self.showCancelButton(False) if self._requestStop: self.setMessage('Encryption canceled!') self.encryptPbar.setValue(self.encryptPbar.minimum()) self._requestStop = False os.remove(gn) return return gn def decrypt(self): "encrypt selected file with key" name, t0 = self.getName(), time.perf_counter() if not name: self.setMessage('No file selected') return if not os.path.exists(os.path.join(self.path, name)): self.setMessage('File does not exist') return key = str(self.keyInput.text()) if len(key) < self.minKeyLen: self.setMessage(('Key must be at least ' '{} characters long').format(self.minKeyLen)) return self.lock() gn = self.decryptFile(key, os.path.join(self.path, name)) if not gn: self.lock(False) return self.decrypted.append(os.path.basename(gn)) self.lock(False) self.populateTable(self.path) # repopulate folder list bn, tt = os.path.basename(gn), time.perf_counter() - t0 self.setMessage('Decrypted, saved "{}"'.format(bn, 13)) self.extraLabel.setText('Decrypting took ' + self.secs_fmt(tt)) def decryptFile(self, key, fn): "decrypts a file using AES (MODE_GCM)" blksize = self.blksize gn = hashlib.md5(os.path.basename(fn).encode()).hexdigest() gn = os.path.join(self.path, gn) # temporary name if os.path.exists(gn): self.setMessage('file already exists') return self.decryptPbar.reset() csz = 0.0 # current processed value chk, fnsz = AES.block_size, os.path.getsize(fn) prog, title = '({:.02%}) {}', self.windowTitle() try: with open(fn, 'rb') as src, open(gn, 'wb') as dst: # extract iv, salt MAC = src.read(16) iv = src.read(AES.block_size * 2) salt = src.read(256) vault = AES.new(self.hashKey(key, salt), AES.MODE_GCM, iv) self.showCancelButton(True) # extract file size, file name length sizes = src.read(struct.calcsize('<2Q')) fsz, fnz = struct.unpack('<2Q', vault.decrypt(sizes)) # extract filename; round up fnz to nearest chk rnz = fnz if not fnz % chk else fnz + chk - fnz % chk rfn = vault.decrypt(src.read(rnz))[:fnz].decode() self.setMessage('Found "{}"'.format(rfn), 13, (255, 211, 127)) while 1: dat = src.read(blksize) if not dat: break dst.write(vault.decrypt(dat)) csz += blksize # show progress self.decryptPbar.setValue(int(round(csz * 100.0 / fnsz))) self.setWindowTitle(prog.format(1 - (csz / fnsz), title)) app.processEvents() if self._requestStop: break if not self._requestStop: dst.truncate(fsz) # remove padding if not self._requestStop: vault.verify(MAC) self.hashLabel.setText('') except (ValueError, KeyError) as err: os.remove(gn) self.setMessage('Invalid decryption!') self.setWindowTitle(title) self.showCancelButton(False) return except Exception as err: os.remove(gn) self.setMessage('Invalid key or file!') self.setWindowTitle(title) self.showCancelButton(False) return self.decryptPbar.setValue(self.decryptPbar.maximum()) self.setWindowTitle(title) self.showCancelButton(False) if self._requestStop: self.setMessage('Decryption canceled!') self.decryptPbar.setValue(self.decryptPbar.minimum()) self._requestStop = False os.remove(gn) return # restore original file name name, ext = os.path.splitext(rfn) count = 1 fn = os.path.join(self.path, name + ext) while os.path.exists(fn): fn = os.path.join(self.path, name + '_{}'.format(count) + ext) count += 1 os.rename(gn, fn) # restore original name return fn # saved name def copyKeyHash(self, action): "copies either the key or the hash to clipboard" act = str(action.text()).lower() if 'key' in act: txt = str(self.keyInput.text()) elif 'hash' in act: txt = str(self.hashLabel.text()) else: self.setMessage('Invalid copy selection') return if not txt: self.setMessage('Empty text; Nothing to copy') return if 'key' in act: self.setMessage('Key copied to clipboard') elif 'hash' in act: self.setMessage('Hash copied to clipboard') else: self.setMessage('Invalid copy selection') return self.clipboard.clear() self.clipboard.setText(txt) def secs_fmt(self, s): "6357 -> '1h 45m 57s'" Y, D, H, M = 31556952, 86400, 3600, 60 y = int(s // Y) s -= y * Y d = int(s // D) s -= d * D h = int(s // H) s -= h * H m = int(s // M) s -= m * M r = (str(int(s)) if int(s) == s else str(round(s, 3))) + 's' if m: r = str(m) + 'm ' + r if h: r = str(h) + 'h ' + r if d: r = str(d) + 'd ' + r if y: r = str(y) + 'y ' + r return r.strip() def closeEvent(self, event): self.clipboard.clear() def setup(self): "constructs the gui" Fixed = QSizePolicy() MinimumExpanding = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.minKeyLen = 8 self.maxKeyLen = 4096 self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Horizontal) self.splitter.splitterMoved.connect(self.splitterChanged) # left column self.leftColumn = QWidget() self.vl01 = QVBoxLayout() # left column - first item (0; horizonal layout 0) self.hl00 = QHBoxLayout() self.hl00.setSpacing(5) self.openButton = QPushButton('&Open') self.openButton.setToolTip('Open folder') self.openButton.setMinimumSize(60, 20) self.openButton.setMaximumSize(60, 20) self.openButton.setSizePolicy(Fixed) self.openButton.clicked.connect(self.getFolder) #ico = self.style().standardIcon(QStyle.SP_DirIcon) #self.openButton.setIcon(ico) self.folderLabel = QLabel() self.folderLabel.setMinimumSize(135, 20) self.folderLabel.setMaximumSize(16777215, 20) self.folderLabel.setSizePolicy(MinimumExpanding) self.hl00.insertWidget(0, self.openButton) self.hl00.insertWidget(1, self.folderLabel) # left column - second item (1) self.folderTable = QTableWidget() self.folderTable.setMinimumSize(200, 32) self.folderTable.horizontalHeader().setVisible(False) self.folderTable.horizontalHeader().setStretchLastSection(True) self.folderTable.verticalHeader().setVisible(False) self.folderTable.verticalHeader().setDefaultSectionSize(15) self.folderTable.itemChanged.connect(self.editFileName) # left column - third item (2) self.extraLabel = QLabel() self.extraLabel.setMinimumSize(200, 20) self.extraLabel.setMaximumSize(16777215, 20) self.extraLabel.setSizePolicy(MinimumExpanding) self.extraLabel.setTextInteractionFlags(Qt.LinksAccessibleByMouse) # finalize left column self.vl01.insertLayout(0, self.hl00) self.vl01.insertWidget(1, self.folderTable) self.vl01.insertWidget(2, self.extraLabel) self.leftColumn.setLayout(self.vl01) # right column self.rightColumn = QWidget() self.vl02 = QVBoxLayout() # right column - first item (0) self.messageLabel = QLabel() self.messageLabel.setMinimumSize(290, 20) self.messageLabel.setMaximumSize(16777215, 20) self.messageLabel.setSizePolicy(MinimumExpanding) self.messageLabel.setAlignment(Qt.AlignCenter) # right column - second item (2; horizontal layout 1) self.hl01 = QHBoxLayout() self.hl01.setSpacing(5) self.encryptButton = QPushButton('&Encrypt') #\U0001F512 self.encryptButton.setToolTip('Encrypt selected file') self.encryptButton.setMinimumSize(60, 20) self.encryptButton.setMaximumSize(60, 20) self.encryptButton.setSizePolicy(Fixed) self.encryptButton.clicked.connect(self.encrypt) self.encryptPbar = QProgressBar() self.encryptPbar.setMinimumSize(225, 20) self.encryptPbar.setMaximumSize(16777215, 20) self.encryptPbar.setSizePolicy(MinimumExpanding) self.encryptPbar.setTextVisible(False) palette = self.encryptPbar.palette() # color of progress bar color = QColor(211, 70, 0) palette.setColor(QPalette.Highlight, color) self.encryptPbar.setPalette(palette) self.hl01.insertWidget(0, self.encryptButton) self.hl01.insertWidget(1, self.encryptPbar) # right column - third item (3; horizontal layout 2) self.hl02 = QHBoxLayout() self.hl02.setSpacing(5) self.cancelButton = QPushButton('C&ANCEL') self.cancelButton.setToolTip('Cancels current operation') self.cancelButton.setMinimumSize(70, 24) self.cancelButton.setMaximumSize(70, 24) self.cancelButton.setSizePolicy(Fixed) self.cancelButton.clicked.connect(self.setCancel) font = self.cancelButton.font() font.setBold(True) self.cancelButton.setFont(font) self.cancelButton.blockSignals(True) self.cancelButton.setEnabled(False) self.cancelButton.hide() self._requestStop = False self.keyInput = QLineEdit() self.keyInput.setMinimumSize(225, 20) self.keyInput.setMaximumSize(16777215, 20) self.keyInput.setSizePolicy(MinimumExpanding) self.keyInput.setPlaceholderText('key') self.keyInput.setMaxLength(self.maxKeyLen) self.keyInput.setAlignment(Qt.AlignCenter) self.keyInput.textEdited.connect(self.showKeyLen) self.genKeyButton = QPushButton('&Gen Key') #\U0001F511 self.genKeyButton.setToolTip('Generate a random key') self.genKeyButton.setMinimumSize(60, 20) self.genKeyButton.setMaximumSize(60, 20) self.genKeyButton.setSizePolicy(Fixed) self.genKeyButton.clicked.connect(self.genKey) self.keySizeSB = QSpinBox() self.keySizeSB.setToolTip('Length of key to generate') self.keySizeSB.setRange(32, 1024) self.keySizeSB.setMinimumSize(40, 20) self.keySizeSB.setMaximumSize(40, 20) self.keySizeSB.setSizePolicy(Fixed) self.keySizeSB.setAlignment(Qt.AlignCenter) self.keySizeSB.setButtonSymbols(QSpinBox.NoButtons) self.keySizeSB.setWrapping(True) self.hl02.insertWidget(0, self.cancelButton) self.hl02.insertWidget(1, self.keyInput) self.hl02.insertWidget(2, self.genKeyButton) self.hl02.insertWidget(3, self.keySizeSB) # right column - fourth item (4; horizontal layout 3) self.hl03 = QHBoxLayout() self.hl03.setSpacing(5) self.decryptButton = QPushButton('&Decrypt') #\U0001F513 self.decryptButton.setToolTip('Decrypt selected file') self.decryptButton.setMinimumSize(60, 20) self.decryptButton.setMaximumSize(60, 20) self.decryptButton.setSizePolicy(Fixed) self.decryptButton.clicked.connect(self.decrypt) self.decryptPbar = QProgressBar() self.decryptPbar.setMinimumSize(225, 20) self.decryptPbar.setMaximumSize(16777215, 20) self.decryptPbar.setSizePolicy(MinimumExpanding) self.decryptPbar.setTextVisible(False) self.decryptPbar.setInvertedAppearance(True) palette = self.decryptPbar.palette() # color of progress bar color = QColor(0, 170, 255) palette.setColor(QPalette.Highlight, color) self.decryptPbar.setPalette(palette) self.hl03.insertWidget(0, self.decryptButton) self.hl03.insertWidget(1, self.decryptPbar) # right column - fifth item (7; horizontal layout 4) self.hl04 = QHBoxLayout() self.hl04.setSpacing(5) self.showKeyCB = QCheckBox('&Show Key') self.showKeyCB.setToolTip('Show/Hide key value') self.showKeyCB.setMinimumSize(75, 20) self.showKeyCB.setMaximumSize(75, 20) self.showKeyCB.setSizePolicy(Fixed) self.showKeyCB.clicked.connect(self.showKey) self.showKeyCB.setChecked(True) self.hashPbar = QProgressBar() self.hashPbar.setMinimumSize(150, 20) self.hashPbar.setMaximumSize(16777215, 20) self.hashPbar.setSizePolicy(MinimumExpanding) self.hashPbar.setTextVisible(False) palette = self.hashPbar.palette() # color of progress bar color = QColor(31, 120, 73) palette.setColor(QPalette.Highlight, color) self.hashPbar.setPalette(palette) self.hashButton = QPushButton('&Hash') self.hashButton.setToolTip('Determine file hash') self.hashButton.setMinimumSize(60, 20) self.hashButton.setMaximumSize(60, 20) self.hashButton.setSizePolicy(Fixed) menu = QMenu(self.hashButton) ico = self.style().standardIcon(QStyle.SP_DialogYesButton) for alg in sorted( filter(lambda x: 'shake' not in x, hashlib.algorithms_guaranteed), key=lambda n: (len(n), sorted(hashlib.algorithms_guaranteed).index(n))): menu.addAction( ico, alg ) # drop shake algs as their .hexdigest requires an argument - the rest don't menu.addAction(ico, 'Party') for i in menu.actions(): i.setIconVisibleInMenu(False) self.hashButton.setMenu(menu) menu.triggered.connect(self.genHash) self.hl04.insertWidget(0, self.showKeyCB) self.hl04.insertWidget(1, self.hashPbar) self.hl04.insertWidget(2, self.hashButton) # right column - sixth item (8; horizontal layout 5) self.hl05 = QHBoxLayout() self.hl05.setSpacing(5) self.copyButton = QPushButton('&Copy') #\U0001F4CB self.copyButton.setToolTip('Copy key or hash to clipboard') self.copyButton.setMinimumSize(60, 20) self.copyButton.setMaximumSize(60, 20) self.copyButton.setSizePolicy(Fixed) menu2 = QMenu(self.copyButton) menu2.addAction('Copy Key') menu2.addAction('Copy Hash') self.copyButton.setMenu(menu2) menu2.triggered.connect(self.copyKeyHash) self.hashLabel = QLabel() self.hashLabel.setMinimumSize(225, 20) self.hashLabel.setMaximumSize(16777215, 20) self.hashLabel.setSizePolicy(MinimumExpanding) self.hashLabel.setTextFormat(Qt.PlainText) self.hashLabel.setAlignment(Qt.AlignCenter) self.hashLabel.setTextInteractionFlags(Qt.TextSelectableByMouse) self.hl05.insertWidget(0, self.copyButton) self.hl05.insertWidget(1, self.hashLabel) # finalize right column self.vl02.insertWidget(0, self.messageLabel) self.vl02.insertSpacerItem(1, QSpacerItem(0, 0)) self.vl02.insertLayout(2, self.hl01) self.vl02.insertLayout(3, self.hl02) self.vl02.insertLayout(4, self.hl03) self.vl02.insertSpacerItem(5, QSpacerItem(0, 0)) self.vl02.insertWidget(6, QFrame()) self.vl02.insertLayout(7, self.hl04) self.vl02.insertLayout(8, self.hl05) self.rightColumn.setLayout(self.vl02) # finalize main window self.splitter.insertWidget(0, self.leftColumn) self.splitter.insertWidget(1, self.rightColumn) layout = QHBoxLayout(self) layout.addWidget(self.splitter) self.setLayout(layout) self.setWindowTitle('Simple File Encryptor/Decryptor') self.resize(self.sizeHint())
class RootsApp(QMainWindow): standard_deviation_threshold = 0.1 # when I receive a measurement from the sensor I check if its standard deviation; if it's too low it means the sensor is not working temporary_database_filename = "temporary.db" # the current session is stored in a temporary database. When the user saves, it is copied at the desired location def __init__(self): super().__init__(); self.setWindowTitle("Roots") self.setFixedWidth(1200) self.resize(1200, 1200) self.threadpool = QThreadPool(); self.object_list = list() self.is_training_on = False self.interaction_under_training = None self.n_measurements_collected = 0 self.n_measurements_to_collect = 3 self.sensor_not_responding = True self.sensor_not_responding_timeout = 2000 # milliseconds self.database_connection = self.create_temporary_database() self.active_object = None self.number_of_objects_added = 0 self.sensor_start_freq = 250000 self.sensor_end_freq = 3000000 # creates the plot self.plotWidget = pyqtgraph.PlotWidget(title = "Sensor Response") self.plotWidget.setFixedHeight(300) self.plotWidget.getAxis("bottom").setLabel("Excitation frequency", "Hz") self.plotWidget.getAxis("left").setLabel("Volts", "V") self.dataPlot = self.plotWidget.plot() # timer used to see if the sensor is responding self.timer = QTimer() self.timer.setInterval(self.sensor_not_responding_timeout) self.timer.timeout.connect(self.timer_timeout) self.timer_timeout() # defines the actions in the file menu with button actions iconExit = QIcon("icons/icon_exit.png") btnActionExit = QAction(iconExit, "Exit", self) btnActionExit.setStatusTip("Click to terminate the program") btnActionExit.triggered.connect(self.exit) iconSave = QIcon("icons/icon_save.ico") buttonActionSave = QAction(iconSave, "Save current set of objects", self) # buttonActionSave.setStatusTip("Click to perform action 2") buttonActionSave.triggered.connect(self.save) iconOpen = QIcon("icons/icon_load.png") buttonActionOpen = QAction(iconOpen, "Load set of objects", self) buttonActionOpen.triggered.connect(self.open) # toolbar toolBar = QToolBar("Toolbar") toolBar.addAction(buttonActionSave) toolBar.addAction(buttonActionOpen) toolBar.setIconSize(QSize(64, 64)) toolBar.setStyleSheet(styles.toolbar) self.addToolBar(toolBar) # menu menuBar = self.menuBar() menuBar.setStyleSheet(styles.menuBar) menuFile = menuBar.addMenu("File") menuOptions = menuBar.addMenu("Options") menuView = menuBar.addMenu("View") menuConnect = menuBar.addMenu("Connect") menuFile.addAction(buttonActionSave) menuFile.addAction(buttonActionOpen) menuFile.addAction(btnActionExit) # status bar self.setStatusBar(QStatusBar(self)) # creates the "My Objects" label labelMyObjects = QLabel("My Objects") labelMyObjects.setFixedHeight(100) labelMyObjects.setAlignment(Qt.AlignCenter) labelMyObjects.setStyleSheet(styles.labelMyObjects) # button "add object" icon_plus = QIcon("icons/icon_add.png") self.btn_create_object = QPushButton("Add Object") self.btn_create_object.setCheckable(False) self.btn_create_object.setIcon(icon_plus) self.btn_create_object.setFixedHeight(80) self.btn_create_object.setStyleSheet(styles.addObjectButton) self.btn_create_object.clicked.connect(self.create_object) # defines the layout of the "My Objects" section self.verticalLayout = QVBoxLayout() self.verticalLayout.setContentsMargins(0,0,0,0) self.verticalLayout.addWidget(labelMyObjects) self.verticalLayout.addWidget(self.btn_create_object) self.spacer = QSpacerItem(0,2000, QSizePolicy.Expanding, QSizePolicy.Expanding) self.verticalLayout.setSpacing(0) self.verticalLayout.addSpacerItem(self.spacer) #adds spacer # defines the ComboBox which holds the names of the objects self.comboBox = QComboBox() self.comboBox.addItem("- no object selected") self.comboBox.currentIndexChanged.connect(self.comboBox_index_changed) self.comboBox.setFixedSize(300, 40) self.comboBox.setStyleSheet(styles.comboBox) self.update_comboBox() # defines the label "Selected Object" (above the comboBox) self.labelComboBox = QLabel() self.labelComboBox.setText("Selected Object:") self.labelComboBox.setStyleSheet(styles.labelComboBox) self.labelComboBox.adjustSize() # vertical layout for the combobox and its label self.VLayoutComboBox = QVBoxLayout() self.VLayoutComboBox.addWidget(self.labelComboBox) self.VLayoutComboBox.addWidget(self.comboBox) # label with the output text (the big one on the right) self.labelClassification = QLabel() self.labelClassification.setText("No interaction detected") self.labelClassification.setFixedHeight(80) self.labelClassification.setStyleSheet(styles.labelClassification) self.labelClassification.adjustSize() HLayoutComboBox = QHBoxLayout() HLayoutComboBox.addLayout(self.VLayoutComboBox) HLayoutComboBox.addSpacerItem(QSpacerItem(1000,0, QSizePolicy.Expanding, QSizePolicy.Expanding)); #adds spacer HLayoutComboBox.addWidget(self.labelClassification) # creates a frame that contains the combobox and the labels frame = QFrame() frame.setStyleSheet(styles.frame) frame.setLayout(HLayoutComboBox) # sets the window layout with the elements created before self.windowLayout = QVBoxLayout() self.windowLayout.addWidget(self.plotWidget) self.windowLayout.addWidget(frame) self.windowLayout.addLayout(self.verticalLayout) # puts everything into a frame and displays it on the window self.mainWindowFrame = QFrame() self.mainWindowFrame.setLayout(self.windowLayout) self.mainWindowFrame.setStyleSheet(styles.mainWindowFrame) self.setCentralWidget(self.mainWindowFrame) self.create_object() # creates one object at the beginning # ----------------------------------------------------------------------------------------------------------- # Shows a welcome message def show_welcome_msg(self): welcome_msg = QMessageBox() welcome_msg.setText("Welcome to the Roots application!") welcome_msg.setIcon(QMessageBox.Information) welcome_msg.setInformativeText(strings.welcome_text) welcome_msg.setWindowTitle("Welcome") welcome_msg.exec_() # ----------------------------------------------------------------------------------------------------------- # When the user changes the object in the combobox, updates the active object def comboBox_index_changed(self, index): object_name = self.comboBox.currentText() for object in self.object_list: if object.name == object_name: self.set_active_object(object) print("DEBUG: selected object changed. Object name: {0}".format(object.name)) return # ----------------------------------------------------------------------------------------------------------- # This function allows to save the current objects on a file def save(self): current_path = os.getcwd() directory_path = current_path + "/Saved_Workspaces" if not os.path.exists(directory_path): os.mkdir(directory_path) file_path = None [file_path, file_extension] = QFileDialog.getSaveFileName(self,"Roots", directory_path, "Roots database (*.db)") if file_path is None: return temp_database_path = current_path + "/" + RootsApp.temporary_database_filename shutil.copyfile(temp_database_path, file_path) # copies the temporary database to save the current workspace return # ----------------------------------------------------------------------------------------------------------- # this function creates a clean database where all the data of this session will be temporarily stored def create_temporary_database(self): current_path = os.getcwd() file_path = current_path + "/" + RootsApp.temporary_database_filename if os.path.exists(file_path): # if the database is already there it deletes it to reset it os.remove(file_path) print("DEBUG: removing database. (in 'RootsApp.create_temporary_database()'") database_connection = database.create_connection(RootsApp.temporary_database_filename) # creates the temporary database database.create_tables(database_connection) # initializes the database database.reset_db(database_connection) # resets the database (not needed but it doesn't cost anything to put it) return database_connection # ----------------------------------------------------------------------------------------------------------- # This function allows to load previously created objects from a file def open(self): current_path = os.getcwd() saved_files_directory = current_path + "/Saved_Workspaces" [file_path, file_extension] = QFileDialog.getOpenFileName(self,"Roots", saved_files_directory, "Roots database (*.db)"); if file_path == '': return for object in self.object_list.copy(): # deletes all the objects print("DEBUG: deleting object {0} (in 'open()')".format(object.name)) self.delete_object(object) temp_database_path = current_path + "/" + RootsApp.temporary_database_filename self.database_connection.close() os.remove(temp_database_path) shutil.copyfile(file_path, temp_database_path) # replaces the temporary database with the file to open self.database_connection = database.create_connection(temp_database_path) object_tuples = database.get_all_objects(self.database_connection) for object_tuple in object_tuples: object_ID, object_name = object_tuple location_IDs = database.get_locations_id_for_object(self.database_connection, object_ID) formatted_location_IDs = [] for location_ID in location_IDs: formatted_location_IDs.append(location_ID[0]) print("DEBUG: loading object {0} with location IDs {1}. (in 'RootsApp.open()')".format(object_name, formatted_location_IDs)) self.add_object(object_name, object_ID, formatted_location_IDs) self.train_classifiers() return # ----------------------------------------------------------------------------------------------------------- # This function updates the ComboBox whenever objects are created, destroyed or the active object has changed def update_comboBox(self): print("DEBUG: repainting ComboBox. (in 'RootsApp.update_comboBox()'") self.comboBox.clear() self.comboBox.addItem("none") for object in self.object_list: self.comboBox.addItem(object.name) self.comboBox.adjustSize() # ----------------------------------------------------------------------------------------------------------- # This is a timer which is restarted every time a measurement is received. If it elapses it means that the sesnor is not connected def timer_timeout(self): print("DEBUG: timer timeout. (in 'RootsApp.timer_timeout()'") self.sensor_not_responding = True self.statusBar().showMessage(strings.sensor_disconnected) self.statusBar().setStyleSheet(styles.statusBarError) self.plotWidget.setTitle("Sensor not connected") # ----------------------------------------------------------------------------------------------------------- # This function creates a new object in the database and then calls the "add_object" function, which adds the newly created object to the application def create_object(self): new_object_name = "Object {0}".format(self.number_of_objects_added + 1) [new_object_ID, location_IDs] = database.create_object(self.database_connection, new_object_name) self.add_object(new_object_name, new_object_ID, location_IDs) # ----------------------------------------------------------------------------------------------------------- # This function deletes an object from the database, and from the application object list. It alsos destroys the object def delete_object(self, object): print("DEBUG: deleting object {0}. (in 'RootsApp.delete_object()')".format(object.ID)) database.delete_object(self.database_connection, object.ID) self.object_list.remove(object) self.verticalLayout.removeItem(object.layout) self.update_comboBox() object.delete() # ----------------------------------------------------------------------------------------------------------- # This function adds an object to the current application. Note that if you want to create an object ex-novo you should call "create_object". This function is useful when loading existing objects from a file def add_object(self, name, object_ID, location_IDs): self.number_of_objects_added += 1 new_object = Object(name, object_ID, location_IDs, self) self.object_list.append(new_object) for ID in location_IDs: # initializes the measurements with 0 if the measurement is empty #print("DEBUG: initializing location ID {0}".format(ID)) measurements = database.get_measurements_for_location(self.database_connection, ID) print("DEBUG: location {0} of object {1} is trained: {2}. (in 'RootsApp.add_object()')".format(ID, new_object.name, database.is_location_trained(self.database_connection, ID))) if len(measurements) == 0: database.save_points(self.database_connection, [0], ID) database.set_location_trained(self.database_connection, ID, "FALSE") elif database.is_location_trained(self.database_connection, ID) == "TRUE": new_object.get_interaction_by_ID(ID).setCalibrated(True) # inserts the newly created object before the "Add Object" button index = self.verticalLayout.indexOf(self.btn_create_object) self.verticalLayout.insertLayout(index, new_object.layout) self.update_comboBox() print("DEBUG: object {0} added. (in 'RootsApp.add_object()')".format(new_object.name)) return # ----------------------------------------------------------------------------------------------------------- # This function takes as input the measurement data and formats it to plot it on the graph def update_graph(self, data): frequency_step = (self.sensor_end_freq - self.sensor_start_freq) / len(data) x_axis = numpy.arange(self.sensor_start_freq, self.sensor_end_freq, frequency_step) formatted_data = numpy.transpose(numpy.asarray([x_axis, data])) self.dataPlot.setData(formatted_data) # ----------------------------------------------------------------------------------------------------------- # This function starts the UDP server that receives the measurements def run_UDP_server(self, UDP_IP, UDP_PORT): self.UDPServer = UDPServer(UDP_IP, UDP_PORT) self.UDPServer.signals.measurementReceived.connect(self.process_measurement) self.threadpool.start(self.UDPServer) # ----------------------------------------------------------------------------------------------------------- # This function changes some global variables to tell the application to save the incoming measurements into the database. The measurements belong to the interaction passed as argument def start_collecting_measurements(self, interaction): if self.sensor_not_responding: print("DEBUG: warning! Can't start calibration, the sensor is not responding! (in 'RootsApp.start_collecting_measurements()')") error_msg = QMessageBox() error_msg.setText("Can't start calibration!") error_msg.setIcon(QMessageBox.Critical) error_msg.setInformativeText('The sensor is not responding, make sure it is connected') error_msg.setWindowTitle("Error") error_msg.exec_() else: print("starting to collect measurements into the database at location ID {0} (in 'RootsApp.start_collecting_measurements()')".format(interaction.ID)); self.is_training_on = True self.interaction_under_training = interaction database.delete_measurements_from_location(self.database_connection, interaction.ID) # resets the location measurements self.progress_dialog = QProgressDialog("Calibrating", "Abort", 0, self.n_measurements_to_collect, self) self.progress_dialog.setWindowModality(Qt.WindowModal) self.progress_dialog.setWindowTitle("Calibration") self.progress_dialog.setFixedSize(400, 200) self.progress_dialog.setValue(0) self.progress_dialog.exec_() # ----------------------------------------------------------------------------------------------------------- # This function is called by the UDP thread every time that a measurement is received. It does the following: # 1. Plots the incoming measurement # 2. IF training mode IS on: # Predicts the interaction (tries to guess where the user is touching) # ELSE: # Saves the measurement and retrains the classifier with the new data def process_measurement(self, received_data): self.sensor_not_responding = False self.plotWidget.setTitle("Sensor response") self.timer.start() # starts the timer that checks if we are receiving data from the sensor measurement = received_data.split(' ') # get rid of separator measurement = [float(i) for i in measurement] # convert strings to float self.update_graph(measurement) self.predict_interaction(measurement) # checks the standard deviation of the received data to see if the sensor is working well if (numpy.std(measurement) < self.standard_deviation_threshold): self.statusBar().showMessage(strings.sensor_not_working) self.statusBar().setStyleSheet(styles.statusBarError) else: self.statusBar().setStyleSheet(styles.statusBar) if self.is_training_on: print("saving measurement {0} into database at location_ID {1}. (in 'RootsApp.process_measurement()')".format(self.n_measurements_collected + 1, self.interaction_under_training.ID)) database.save_points(self.database_connection, measurement, self.interaction_under_training.ID) self.n_measurements_collected += 1 self.progress_dialog.setValue(self.n_measurements_collected) if (self.n_measurements_collected >= self.n_measurements_to_collect): self.is_training_on = False self.n_measurements_collected = 0 print("DEBUG: {0} measurements were saved at location_ID {1}. (in 'RootsApp.process_measurement()')".format(self.n_measurements_to_collect, self.interaction_under_training.ID)) self.train_classifiers() self.interaction_under_training.setCalibrated(True) # this makes the button "Calibrate" change coulour # ----------------------------------------------------------------------------------------------------------- # This function retrains the classifiers using all the measurements present in the database and assigns to each object its classifier def train_classifiers(self): #[objects_ID, classifiers] classifiers = classifier.get_classifiers(self.database_connection) print("DEBUG: the following classifiers were created: {0}. (in 'RootsApp.train_classifiers')".format(classifiers)) for object in self.object_list: for index, tuple in enumerate(classifiers): object_ID, classif = tuple; # extracts the object ID and the classifier from the tuple if object_ID == object.ID: object.classifier = classif del classifiers[index] # ----------------------------------------------------------------------------------------------------------- # This function changes the current active object (the software tries to guess where the user is touching using the calibration data from the active object) def set_active_object(self, active_object): self.active_object = active_object for obj in self.object_list: if obj == active_object: active_object.set_highlighted(True) else: obj.set_highlighted(False) index = self.comboBox.findText(self.active_object.name) # updates the index of the ComboBox self.comboBox.setCurrentIndex(index) # ----------------------------------------------------------------------------------------------------------- # This function changes the name of an object. It updates the database AND the application data structure. def rename_object(self, object, new_name): print("DEBUG: changing name of object '{0}' (in 'RootsApp.rename_object')".format(object.name)) object.set_name(new_name) database.rename_object(self.database_connection, object.ID, new_name) self.update_comboBox() # ----------------------------------------------------------------------------------------------------------- # This function uses the classifier of the active object to guess where the user is touching, based on the incoming measurement def predict_interaction(self, measurement): if (len(self.object_list) <= 0): self.labelClassification.setText("No objects available") self.statusBar().showMessage(strings.no_objects) return if self.active_object is None: self.labelClassification.setText("No object selected") self.statusBar().showMessage(strings.no_object_selected) return if self.active_object.classifier is None: self.labelClassification.setText("The object is not calibrated") self.statusBar().showMessage(strings.object_not_calibrated) return else: predicted_interaction_id = self.active_object.classifier(measurement) interaction = self.active_object.get_interaction_by_ID(predicted_interaction_id) self.labelClassification.setText(interaction.name) self.statusBar().showMessage("") #print("DEBUG: predicted interaction ID: ", interaction.ID) # ----------------------------------------------------------------------------------------------------------- # This is a system event that gets called whenever the user tries to close the application. It calls the "exit()" # function (just below) to open a dialog to make sure the user really wants to quit. def closeEvent(self, event): if not self.exit(): event.ignore() # ----------------------------------------------------------------------------------------------------------- # This function gets called when the user cliks on the "Exit" button in the "File" menu or when it tries to close the window (indirectly) # Here we open a dialog to make sure the user really wants to quit. def exit(self): dialogWindow = DialogExit() answer = dialogWindow.exec_() if (answer == True): self.UDPServer.stop() self.close() return answer