def mark_all_as_read(self): msg = QMessageBox(self.app.window.ui) msg.setIcon(QMessageBox.Question) msg.setWindowTitle("Mark all as read ...") msg.setText("Are you sure you want all posts marked as read?") msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) if msg.exec_() == QMessageBox.Yes: self.app.window.update_messages_as_read() self.app.db.set_all_unread_status(False) self.update()
def getErrorDialog(self,text,infoText,detailedText): dlg = QMessageBox(self) dlg.setIcon(QMessageBox.Warning) dlg.setWindowModality(QtCore.Qt.WindowModal) dlg.setWindowTitle("Error") dlg.setText(text) dlg.setInformativeText(infoText) dlg.setDetailedText(detailedText) dlg.setStandardButtons(QMessageBox.Ok) return dlg
def QuestionDialog(title, text, info = None, dontAsk = False): msgBox = QMessageBox() buttonYes = msgBox.addButton(_("Yes"), QMessageBox.ActionRole) buttonNo = msgBox.addButton(_("No"), QMessageBox.ActionRole) answers = {buttonYes:"yes", buttonNo :"no"} if dontAsk: buttonDontAsk = msgBox.addButton(_("Don't ask again"), QMessageBox.ActionRole) answers[buttonDontAsk] = "dontask" msgBox.setText(text) if not info: info = _("Do you want to continue?") msgBox.setInformativeText(info) dialog = Dialog(_(title), msgBox, closeButton = False, isDialog = True, icon="question") dialog.resize(300,120) dialog.exec_() ctx.mainScreen.processEvents() if msgBox.clickedButton() in answers.keys(): return answers[msgBox.clickedButton()] return "no"
def displayAbout(self): msgBox = QMessageBox() msgBox.setTextFormat(Qt.RichText) msgBox.setWindowTitle("Data Extractor for reddit") msgBox.setText(""" <p>This program uses the following open source software:<br> <a href="http://www.riverbankcomputing.co.uk/software/pyqt/intro">PyQt</a> under the GNU GPL v3 license <br> <a href="https://praw.readthedocs.org/en/v2.1.16/">PRAW (Python Reddit API Wrapper)</a> under the GNU GPL v3 license <br> <a href="http://docs.python-requests.org/en/latest/">Requests</a> under the Apache2 license <br> <a href="http://www.crummy.com/software/BeautifulSoup/">Beautiful Soup</a> under a simplified BSD licence <br> <a href="https://github.com/rg3/youtube-dl">youtube-dl</a> under an unlicense (public domain) </p> <p>This program makes use of a modified version of <a href="https://www.videolan.org/vlc/">VLC's</a> logo:<br> Copyright (c) 1996-2013 VideoLAN. This logo or a modified version may<br> be used or modified by anyone to refer to the VideoLAN project or any<br> product developed by the VideoLAN team, but does not indicate<br> endorsement by the project. </p> <p>This program makes use of a modified version of Microsoft Window's<br> .txt file icon. This is solely the property of Microsoft Windows<br> and I claim no ownership. </p> <p>This program is released under the GNU GPL v3 license<br> <a href="https://www.gnu.org/licenses/quick-guide-gplv3.html">GNU GPL v3 license page</a><br> See <a href="https://github.com/NSchrading/redditDataExtractor/blob/master/LICENSE.txt">LICENSE.txt</a> for more information. </p> """) msgBox.exec()
def show_err_dialog(s): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText(s) msg.setWindowTitle("Warning!") msg.setStandardButtons(QMessageBox.Ok) msg.exec_()
def QuestionDialog(title, text, info=None, dontAsk=False): msgBox = QMessageBox() buttonYes = msgBox.addButton(_("Yes"), QMessageBox.ActionRole) buttonNo = msgBox.addButton(_("No"), QMessageBox.ActionRole) answers = {buttonYes: "yes", buttonNo: "no"} if dontAsk: buttonDontAsk = msgBox.addButton(_("Don't ask again"), QMessageBox.ActionRole) answers[buttonDontAsk] = "dontask" msgBox.setText(text) if not info: info = _("Do you want to continue?") msgBox.setInformativeText(info) dialog = Dialog(_(title), msgBox, closeButton=False, isDialog=True, icon="question") dialog.resize(300, 120) dialog.exec_() ctx.mainScreen.processEvents() if msgBox.clickedButton() in answers.keys(): return answers[msgBox.clickedButton()] return "no"
def install(self): pref = self.app.preferences.ui ct = pref.configTable curi = int(pref.filtersComboBox_new.currentIndex()) filter = self._filters[self._keys[curi]] config, hash = {}, gen_hash() settings = self.filter_settings(filter.id, hash) for i in range(ct.rowCount()): config[unicode(ct.item(i,0).text())] = unicode(ct.item(i,1).text()) try: filter.install(settings, config) except Exception as e: msg = QMessageBox(pref) msg.setIcon(QMessageBox.Critical) msg.setWindowTitle("Installation Error ...") msg.setText("An Error occured during installation.") msg.setInformativeText("Could install filter '%s'." % filter.name) msg.setStandardButtons(QMessageBox.Ok) msg.setDetailedText(format_exc()) msg.exec_() return # success self.add_filter_instance(filter, hash) pref.filtersComboBox_new.currentIndexChanged.emit(curi) pref.filtertabWidget.setCurrentIndex(0)
def _save_dialog(self, parent, title, msg, det_msg=''): d = QMessageBox(parent) d.setWindowTitle(title) d.setText(msg) d.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) return d.exec_()
def displayDialog(message, detailedMessage=None, title=None, level=logging.CRITICAL, dialogWidth=150): ''' A generic function to display the dialog in a QMessageBox message: Primary message to be displayed detailedMessage: detail message that is shown after user clicks on the button level: Message level dialogWidth: width of the messageBox ''' width = dialogWidth mainWin = WorkbenchHelper.window msgBox = QMessageBox (parent=mainWin) if detailedMessage: detailedMessage = 'Application ' + " encountered following problem :" + detailedMessage if len(message) < width: message += ' ' * (width - len(message)) msgBox.setWindowTitle(__getTitleFromLevel(level)) msgBox.setIcon(__getIconFromLevel(level)) msgBox.setText(message) if detailedMessage: msgBox.setDetailedText(detailedMessage) ret = msgBox.exec_()
def show_popup(set_text, detailed, window_title, message_type): mess = QMessageBox() mess.setText(set_text) if len(detailed) > 0: mess.setInformativeText("For more information click \"Show Details\"") mess.setDetailedText(detailed) mess.setIcon(message_type) mess.setWindowTitle(window_title) mess.setMinimumWidth(500) mess.exec_()
def confirmDialog(message): """ Make a simple Yes / No confirmation dialog box with a message :type message: str """ msgBox = QMessageBox() msgBox.setText(message) msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgBox.setDefaultButton(QMessageBox.No) return msgBox
def msgbox(hsgui, title, msg): # Make a non modal critical QMessageBox msgBox = QMessageBox( hsgui ); msgBox.setAttribute( Qt.WA_DeleteOnClose ) msgBox.setStandardButtons( QMessageBox.Ok ) msgBox.setWindowTitle( title ) msgBox.setText( msg ) msgBox.setModal( False ) msgBox.open( msgBox.close ) msgBox.show() hsgui.non_modal_qt_handles.append(msgBox)
def emergency_msgbox(title, msg): 'Make a non modal critical QMessageBox.' from PyQt4.Qt import QMessageBox msgBox = QMessageBox(None) msgBox.setAttribute(Qt.WA_DeleteOnClose) msgBox.setStandardButtons(QMessageBox.Ok) msgBox.setWindowTitle(title) msgBox.setText(msg) msgBox.setModal(False) msgBox.open(msgBox.close) msgBox.show() return msgBox
def startDeleting(self): reply = QtGui.QMessageBox.warning( self, "Caution: Deleting Permanently", "You are about to PERMANENTLY remove accounts and ALL the saved data related to those accounts. Are you sure you want to continue?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: delDate = self.ui.delDate.date().toPyDate() UsrFuncs.cleanOutDeletedAccounts(delDate) alert = QMessageBox() alert.setWindowTitle("Complete") alert.setText("Clean up completed") alert.exec_() return
def handleInfo(self): msg = QMessageBox() #msg.setFixedSize(500, 300) #msg.setGeometry(100,100, 400, 200) msg.setIcon(QMessageBox.Information) msg.setText("Axel Schneider") msg.setInformativeText(unicode(u"©2016")) msg.setWindowTitle("Cut Video") msg.setDetailedText("Python Qt4") msg.setStandardButtons(QMessageBox.Ok) retval = msg.exec_() print "value of pressed message box button:", retval
def sendBookData(self, author, title, genre, genre2, dateRead, grade, comments): if self.dialog.isHidden(): global sock try: sock.send('SEND_ROW_DATA "' + author + '", "' + title + '", "' + genre + '", "' + genre2 + '", ' + dateRead + ', ' + grade + ', "' + comments + '"\n') data = sock.recv(1024) if data == '200 OK': dialog = QMessageBox() dialog.setText("Data added successfully") dialog.exec_() except socket.timeout: dialog = QMessageBox() dialog.set dialog.setText("Connection timed out...") dialog.exec_() except: self.parent().setWindowTitle("Book Library (Disconnected)") dialog = QMessageBox() dialog.set dialog.setText("Connect to server to be able to send data.") dialog.exec_() else: dialog = QMessageBox() dialog.set dialog.setText("Connect to server to be able to send data.") dialog.exec_()
def InfoDialog(text, button=None, title=None, icon="info"): if not title: title = _("Information") if not button: button = _("OK") msgBox = QMessageBox() buttonOk = msgBox.addButton(button, QMessageBox.ActionRole) msgBox.setText(text) dialog = Dialog(_(title), msgBox, closeButton = False, isDialog = True, icon = icon) dialog.resize(300,120) dialog.exec_() ctx.mainScreen.processEvents()
def InfoDialog(text, button=None, title=None, icon="info"): if not title: title = _("Information") if not button: button = _("OK") msgBox = QMessageBox() buttonOk = msgBox.addButton(button, QMessageBox.ActionRole) msgBox.setText(text) dialog = Dialog(_(title), msgBox, closeButton=False, isDialog=True, icon=icon) dialog.resize(300, 120) dialog.exec_() ctx.mainScreen.processEvents()
def checkSaveState(self): close = False if self._unsavedChanges: msgBox = QMessageBox() msgBox.setText("A list or setting has been changed.") msgBox.setInformativeText("Do you want to save your changes?") msgBox.setStandardButtons(QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) msgBox.setDefaultButton(QMessageBox.Save) ret = msgBox.exec_() if ret == QMessageBox.Save: self.saveState() close = True elif ret == QMessageBox.Discard: close = True elif ret == QMessageBox.Cancel: close = False else: close = False else: close = True return close
def viewRemainingImgurRequests(self): msgBox = QMessageBox() msgBox.setWindowTitle("Data Extractor for reddit") if self._rddtDataExtractor.imgurAPIClientID is not None: headers = { 'Authorization': 'Client-ID ' + self._rddtDataExtractor.imgurAPIClientID } apiURL = "https://api.imgur.com/3/credits" requestsSession = requests.session() requestsSession.headers[ 'User-Agent'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36' json = exceptionSafeJsonRequest( requestsSession, apiURL, headers=headers, stream=True, verify='RedditDataExtractor/cacert.pem') if json is not None and json.get('data') is not None and json.get( 'data').get('ClientRemaining'): msgBox.setText("You have " + str(json.get('data').get('ClientRemaining')) + " requests remaining.") else: msgBox.setText( "A problem occurred using the Imgur API. Check that you are connected to the internet and make sure your client-id is correct." ) else: msgBox.setText( "You do not currently have an Imgur client-id set. To set one, go to settings and check 'Change / Reset Client-id'" ) msgBox.exec()
def viewRemainingImgurRequests(self): msgBox = QMessageBox() msgBox.setWindowTitle("Data Extractor for reddit") if self._rddtDataExtractor.imgurAPIClientID is not None: headers = {'Authorization': 'Client-ID ' + self._rddtDataExtractor.imgurAPIClientID} apiURL = "https://api.imgur.com/3/credits" requestsSession = requests.session() requestsSession.headers[ 'User-Agent'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36' json = exceptionSafeJsonRequest(requestsSession, apiURL, headers=headers, stream=True, verify='RedditDataExtractor/cacert.pem') if json is not None and json.get('data') is not None and json.get('data').get('ClientRemaining'): msgBox.setText("You have " + str(json.get('data').get('ClientRemaining')) + " requests remaining.") else: msgBox.setText( "A problem occurred using the Imgur API. Check that you are connected to the internet and make sure your client-id is correct.") else: msgBox.setText( "You do not currently have an Imgur client-id set. To set one, go to settings and check 'Change / Reset Client-id'") msgBox.exec()
class OpenCellId(QtGui.QMainWindow, Ui_MainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) Ui_MainWindow.__init__(self) self.setupUi(self) #Exit Actions self.actionExit.triggered.connect(QtGui.qApp.quit) #Select CSV File for import self.actionImport.triggered.connect(self.importFileSelect) self.select_csv_button.clicked.connect(self.importFileSelect) #Select SQLite file for export self.actionExport.triggered.connect(self.exportFileSelect) self.select_sql_export_button.clicked.connect(self.exportFileSelect) #Initiate import-export action (CSV2SQLite) self.import_button.clicked.connect(self.exportSQLite) #Select SQLite input file for filtering self.select_sql_source_button.clicked.connect(self.filterSelectFile) #Exporting filtered files self.export_button.clicked.connect(self.filterOutput) #search Nominatim for Bounding Box self.bb_nominatim_button.clicked.connect(self.bbNominatim) self.show_bounding_box.clicked.connect(self.showBoundingBox) def showBoundingBox(self): if (self.lat1_input.text() != "" and self.lat2_input.text() != ""\ and self.long1_input.text() != "" and self.long2_input.text() != ""): kml = simplekml.Kml() b_box = kml.newgroundoverlay( name=smart_unicode(self.bounding_box_entry.text())) b_box.color = '371400FF' #this is transparent red b_box.latlonbox.north = float(self.lat2_input.text()) b_box.latlonbox.south = float(self.lat1_input.text()) b_box.latlonbox.east = float(self.long2_input.text()) b_box.latlonbox.west = float(self.long1_input.text()) #save kml file with name based on the full location name kml_filename = smart_unicode( self.bounding_box_entry.text()).replace(', ', '-').replace( ' ', '_') + '_bounding_box.kml' kml.save(kml_filename) Popen( '"C:/Program Files (x86)/Google/Google Earth Pro/client/googleearth.exe" "{}"' .format(kml_filename), stdin=None, stdout=None, stderr=None, close_fds=True, shell=True) else: self.popupWindow( "No Coordinates For Box", "You are missing one or more coordinates for your bounding box. Try searching a location to populate lat/long values." ) def importFileSelect(self): import_name = QtGui.QFileDialog.getOpenFileName() if import_name != "": self.file_import_box.setText(import_name) def exportFileSelect(self): export_name = QtGui.QFileDialog.getSaveFileName() if export_name != "": self.file_export_box.setText(export_name) def exportSQLite(self): db_filename = str(self.file_export_box.text().toUtf8()) if os.path.isfile(db_filename): os.remove(db_filename) csv_filename = str(self.file_import_box.text().toUtf8()) con = sqlite3.Connection(db_filename) cur = con.cursor() cur.execute( 'CREATE TABLE "towers" ("radio" varchar(12), "mcc" varchar(12), "net" varchar(12),"area" varchar(12),"cell" varchar(12),"unit" varchar(12),"lon" varchar(12),"lat" varchar(12),"range" varchar(12),"samples" varchar(12),"changeable" varchar(12),"created" varchar(12),"updated" varchar(12),"averageSignal" varchar(12));' ) input_file = open(csv_filename) check = input_file.readline() if check.split(',')[0] != "radio": input_file.seek(0) csv_reader = csv.reader(input_file, delimiter=',') cur.executemany( 'INSERT INTO towers VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', csv_reader) cur.close() con.commit() con.close() input_file.close() #just let the user know the file was created successfully self.popupWindow('Successful Conversion', 'File successfully converted to SQLite ') def popupWindow(self, title, message): self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Information) self.msg.setWindowTitle(title) self.msg.setText(message) self.msg.setStandardButtons(QMessageBox.Ok) self.msg.exec_() def filterSelectFile(self): filter_input_file = str(QtGui.QFileDialog.getOpenFileName()) if filter_input_file != "": self.source_SQLite_box.setText(filter_input_file) def filterOutput(self): output_base = str(QtGui.QFileDialog.getSaveFileName()) count = 0 CREATE_SQLITE = False CREATE_KMZ = False CREATE_CSV = False if self.export_sqlite_check.isChecked(): CREATE_SQLITE = True if os.path.isfile('output_base' + '.db'): self.popupWindow( "SQLite Database already exists", "This file exist already. This will append to the current DB, adding the table if necessary." ) output_con = sqlite3.Connection(output_base + '.db') output_cur = output_con.cursor() output_cur.execute( 'CREATE TABLE IF NOT EXISTS "towers" ("radio" varchar(12), "mcc" varchar(12), "net" varchar(12),"area" varchar(12),"cell" varchar(12),"unit" varchar(12),"lon" varchar(12),"lat" varchar(12),"range" varchar(12),"samples" varchar(12),"changeable" varchar(12),"created" varchar(12),"updated" varchar(12),"averageSignal" varchar(12));' ) if self.export_kmz_check.isChecked(): CREATE_KMZ = True if os.path.isfile(output_base + '.kmz'): self.popupWindow('Already Existing File', 'The KMZ file already exists. Will replace.') os.remove(output_base + '.kmz') kml = simplekml.Kml() if self.export_csv_check.isChecked(): CREATE_CSV = True csv_file = open(output_base + '.csv', 'wb') writer = csv.writer(csv_file, delimiter=',') writer.writerow(('radio', 'mcc', 'net', 'area', 'cell', 'unit', 'lon', 'lat', 'range', 'samples', 'changeable', 'created', 'updated', 'averageSignal')) #read lat/long values and get them in the proper order try: lat1 = float(self.lat1_input.text().toUtf8()) lat2 = float(self.lat2_input.text().toUtf8()) long1 = float(self.long1_input.text().toUtf8()) long2 = float(self.long2_input.text().toUtf8()) if max(lat1, lat2) == max(abs(lat1), abs(lat2)): pass else: lat1, lat2 = lat2, lat1 if max(long1, long2) == max(abs(long1), abs(long2)): pass else: long1, long2 = long2, long1 if lat1 != '' and lat2 != '' and long1 != '' and long2 != '': loc_string = ' AND lat BETWEEN {a} AND {b} AND lon BETWEEN {c} AND {d}'.format( a=lat1, b=lat2, c=long1, d=long2) else: loc_string = '' except ValueError: lat1, lat2, long1, long2 = '', '', '', '' loc_string = '' if self.MCC_input.text().toUtf8() == "": mob_cc = ' LIKE "%"' else: mob_cc = '={a}'.format(a=self.MCC_input.text().toUtf8()) if self.MNC_input.text().toUtf8() == "": mnc = ' LIKE "%"' else: mnc = '={a}'.format(a=self.MNC_input.text().toUtf8()) if self.LAC_input.text().toUtf8() == "": lac = ' LIKE "%"' else: lac = '={a}'.format(a=self.LAC_input.text().toUtf8()) if self.CID_input.text().toUtf8() == "": cid = ' LIKE "%"' else: cid = '={a}'.format(a=self.CID_input.text().toUtf8()) input_con = sqlite3.Connection( str(self.source_SQLite_box.text().toUtf8())) input_cur = input_con.cursor() query = 'SELECT * FROM towers WHERE mcc{a} AND net{b} AND area{c} AND cell{d}{e};'.format( a=mob_cc, b=mnc, c=lac, d=cid, e=loc_string) input_cur.execute(query) def ResultIter(cursor, array_size=5000): while True: results = cursor.fetchmany(array_size) if not results: break for result in results: yield result for result in ResultIter(input_cur): if CREATE_SQLITE: output_cur.executemany( 'INSERT INTO towers VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', (result, )) if CREATE_KMZ: desc = '{a}:{b}:{c}:{d}'.format(a=result[1], b=result[2], c=result[3], d=result[4]) nm = desc #'CID: {a}'.format(a=result[4]) pnt = kml.newpoint(name = nm, description = desc, coords = \ [(float(result[6]), float(result[7]))]) pnt.style.iconstyle.icon.href = 'greentower.png' if CREATE_CSV: writer.writerow(result) if CREATE_SQLITE: count += 1 output_con.commit() output_con.close() if CREATE_CSV: count += 1 csv_file.close() if CREATE_KMZ: count += 1 kml.save('doc.kml') zf = zipfile.ZipFile(output_base + '.kmz', 'a') zf.write('doc.kml') zf.write('greentower.png') os.remove('doc.kml') zf.close() if os.path.getsize(output_base + '.kmz') > 10000000: self.popupWindow( 'Enormous KMZ file', 'Your KMZ file is enormous. Google Earth may have problems opening it in a reasonable fashion.' ) if self.open_earth_button.isChecked(): Popen( '"C:\Program Files (x86)\Google\Google Earth Pro\client\googleearth.exe" "{}"' .format(output_base + '.kmz'), stdin=None, stdout=None, stderr=None, close_fds=True) if count == 0: self.popupWindow('Unsuccessful', 'No files were exported') else: self.popupWindow('Successful File Exports', str(count) + ' files were exported. ') input_con.close() def bbNominatim(self): geolocator = Nominatim() location_name = self.bounding_box_entry.text().toUtf8() location = geolocator.geocode(location_name, language='en') try: geo_box = location.raw[u'boundingbox'] self.lat1_input.setText(geo_box[0].encode('utf-8')) self.lat2_input.setText(geo_box[1].encode('utf-8')) self.long1_input.setText(geo_box[2].encode('utf-8')) self.long2_input.setText(geo_box[3].encode('utf-8')) self.bounding_box_entry.setText( smart_unicode(location.raw[u'display_name'])) except AttributeError: self.bounding_box_entry.setText( '''No location found. Maybe it's you.''') self.popupWindow( 'Location Not Found', '''I can't find that location. Maybe you can try going back in time and learning how to spell.''' )
class DecoderMain(QtGui.QMainWindow, Ui_MainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) Ui_MainWindow.__init__(self) self.setupUi(self) #just sets the theme of the UI, in this case cleanlooks QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('cleanlooks')) #menu options self.actionExit.triggered.connect(self.closeApplication) self.actionSave.triggered.connect(self.saveOutput) #buttons #all other button actions are handled via the UI directly self.execute_btn.clicked.connect(self.decoderSelect) self.save_btn.clicked.connect(self.saveOutput) #turn on statusBar below self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) self.updateStatus() #initially set the hash and length options to disabled, unless #if the proper function is chosen, then enable the options #there are two things here, both of which serve fine #one hides the entire group, the other disables it self.hash_options_group.hide() #self.hash_options_group.setEnabled(False) #this just deactivates, but doesn't hide self.length_group.hide() #if the user changes the combo box, run the function to #update the show/hide or enable/disabled status of the #hash options and/or length options self.func_select.currentIndexChanged.connect(self.enableOptions) #close the application, however that may happen def closeApplication(self): choice = QtGui.QMessageBox.question(self, 'Exit', 'Exit the Super Decoder?',\ QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) if choice == QtGui.QMessageBox.Yes: exit() else: return def updateStatus(self): message = 'Input Length: {} Output Length: {}'.\ format(len(self.input_line.text()),len(self.output_box.toPlainText())) self.statusBar.showMessage(message) #generic popup window for messages and good times def popupWindow(self, title, message): self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Information) self.msg.setWindowTitle(title) self.msg.setText(message) self.msg.setStandardButtons(QMessageBox.Ok) self.msg.exec_() #save whatever output data there is to a text file #if there is none, it won't save def saveOutput(self): output_text = unicode(self.output_box.toPlainText()) if len(output_text) != 0: output_text = 'Input Text:\n{}\n\nOutput Text:\n{}'.format( self.input_line.text(), output_text) export_name = QtGui.QFileDialog.getSaveFileName( filter=self.tr("Text file (*.txt)")) if export_name != "": f = open(export_name, 'wb') f.write(output_text) f.close() self.popupWindow( 'File Saved', 'Data has been saved to {}. '.format(export_name)) else: self.popupWindow('No Data for Export', 'Sorry, there is no data to save.') #enable or disable the options groups for different functions def enableOptions(self): #hide or show hash options if self.func_select.currentText() == 'Hash Text': self.hash_options_group.show() else: self.hash_options_group.hide() #hide or show length options, depending on function #could do this as one big if statement, but... if self.func_select.currentText() == 'Hex to ASCII': self.length_group.show() self.pad_radio.hide() elif self.func_select.currentText() == 'Base64 Decode': self.length_group.show() self.pad_radio.show() elif self.func_select.currentText() == 'Reverse Nibble': self.length_group.show() self.pad_radio.show() elif self.func_select.currentText() == 'Switch Endianness': self.length_group.show() self.pad_radio.show() elif self.func_select.currentText() == 'Hex to Decimal IP': self.length_group.hide() self.pad_radio.hide() else: self.length_group.hide() #checks the state of the combo box "func_select" #to determine which function to run def decoderSelect(self): self.updateStatus() if self.func_select.currentText() == 'Decimal to Hex': self.decimaltoHex() elif self.func_select.currentText() == 'Decimal to Binary': self.decimaltoBinary() elif self.func_select.currentText() == 'ASCII to Hex': self.asciitoHex() elif self.func_select.currentText() == 'Hex to ASCII': self.hextoAscii() elif self.func_select.currentText() == 'Base64 Encode': self.base64Encode() elif self.func_select.currentText() == 'Base64 Decode': self.base64Decode() elif self.func_select.currentText() == 'Reverse Nibble': self.reverseNibble() elif self.func_select.currentText() == 'Switch Endianness': self.switchEndian() elif self.func_select.currentText() == 'ROT13': self.rot13() elif self.func_select.currentText() == 'Hash Text': self.hashText() elif self.func_select.currentText() == 'Find OUI Vendor': self.findOUIVendor() elif self.func_select.currentText() == 'Hex to Decimal IP': self.hexToDecIP() #convert decimal to hex, pad with leading zero if necessary def decimaltoHex(self): try: input_num = int(self.input_line.text()) except ValueError: self.popupWindow('Invalid Input', 'Sorry, input is not proper decimal. ') self.output_box.clear() return hex_num = hex(input_num)[2:] hex_num = '0' * (len(hex_num) % 2) + hex_num self.output_box.setText(hex_num.rstrip('L')) self.updateStatus() #convert decimal to binary, pad zeroes depending on bit length def decimaltoBinary(self): try: input_num = int(self.input_line.text()) except ValueError: self.popupWindow('Invalid Input', 'Sorry, input is not proper decimal. ') self.output_box.clear() return bits = input_num.bit_length() zero_pad = '0' * (4 - (bits % 4)) bin_num = bin(input_num)[2:] bin_num = zero_pad + bin_num self.output_box.setText(bin_num) self.updateStatus() #encode base64 def base64Encode(self): input_text = unicode(self.input_line.text()) output_text = b64encode(input_text) self.output_box.setText(output_text) self.updateStatus() #decode base64, check length, etc. def base64Decode(self): input_text = unicode(self.input_line.text()) #check if the input has a length that's a multiple of 4 #pad if necessary if len(input_text) % 4 != 0: pad_length = len(input_text) % 4 input_text += '=' * pad_length self.input_line.setText(input_text) try: output_text = b64decode(input_text) except TypeError: self.output_box.clear() self.popupWindow('Invalid Input', 'Sorry, input is not proper base64. ') return self.output_box.setText(output_text) self.updateStatus() #reverse nibble stuff, check length def reverseNibble(self): input_text = unicode(self.input_line.text()) #check to see if input length is multiple of 2 #depending on the radio button selected, it #will truncate, pad, or refuse to decode if len(input_text) % 2: if self.truncate_radio.isChecked(): self.popupWindow('Improper Input Length',\ 'Input length is not a multiple of 2. Truncating. ') elif self.pad_radio.isChecked(): self.popupWindow('Improper Input Length',\ 'Input length is not a multiple of 2. Padding with "F". ') input_text += "F" elif self.refuse_radio.isChecked(): self.popupWindow('Improper Input Length',\ 'Input length is not a multiple of 2. Failure to decode. ') self.output_box.clear() return output_text = ''.join([y + x for x, y in zip(*[iter(input_text)] * 2)]) self.output_box.setText(output_text) self.updateStatus() #switch from LE to BE and vice versa def switchEndian(self): input_text = unicode(self.input_line.text()) if len(input_text) == 0: return if len(input_text) % 2: if self.truncate_radio.isChecked(): input_text = input_text[:-1] self.popupWindow('Improper Input Length',\ 'Input length is not a multiple of 2. Truncating. ') elif self.pad_radio.isChecked(): input_text += 'F' self.popupWindow('Improper Input Length',\ 'Input length is not a multiple of 2. Padding with "F". ') elif self.refuse_radio.isChecked(): self.popupWindow('Improper Input Length',\ 'Input length is not a multiple of 2. Failure to decode. ') self.output_box.clear() return self.output_box.setText("".join( reversed( [input_text[i:i + 2] for i in range(0, len(input_text), 2)]))) self.updateStatus() #get all the hashes of the input text def hashText(self): input_text = unicode(self.input_line.text()) output_text = '' if self.crc32_check.isChecked(): crc32_hash = hex((crc32(input_text) + (1 << 32)) % (1 << 32))[2:-1].upper().zfill(8) output_text += 'CRC32 Hash: {}\n'.format(crc32_hash) if self.adler_check.isChecked(): adler32_hash = hex(adler32(input_text))[2:].upper().zfill(8) output_text += 'Adler32 Hash: {}\n'.format(adler32_hash) if self.md5_check.isChecked(): md5_hash = md5(input_text).hexdigest() output_text += 'MD5 Hash: {}\n'.format(md5_hash) if self.sha1_check.isChecked(): sha1_hash = sha1(input_text).hexdigest() output_text += 'SHA1 Hash: {}\n'.format(sha1_hash) if self.sha256_check.isChecked(): sha256_hash = sha256(input_text).hexdigest() output_text += 'SHA256 Hash: {}\n'.format(sha256_hash) if self.b64_256_check.isChecked(): sha256_64_hash = b64encode(sha256(input_text).digest()) output_text += 'Base64 SHA256 Hash: {}\n'.format(sha256_64_hash) self.output_box.setText(output_text.rstrip()) self.updateStatus() #get a vendor for a given mac address or OUI using sqlite db def findOUIVendor(self): #remove colons, dashes, uppercase and only take first 3 bytes (6 characters when it's text) input_text = unicode(self.input_line.text()).replace(':', '').replace( '-', '').upper()[0:6] #just gonna see if it's hex or not by trying to int it try: int(input_text, 16) except ValueError: self.popupWindow('Invalid Input', 'Sorry, input is not a proper MAC or OUI. ') self.output_box.clear() return result = mutator(input_text) output_text = 'Original OUI: {}\nMatching OUI: {}\nVendor: {}\nMutation: {}\n'.\ format(result[0], result[1], result[2], result[3]) self.output_box.setText(output_text) self.updateStatus() #convert ascii to hex def asciitoHex(self): input_text = self.input_line.text().encode('utf8') output_text = hexlify(input_text).upper() self.output_box.setText(output_text) self.updateStatus() #convert hex to ascii, check for validity def hextoAscii(self): valid_chars = 'ABCDEF0123456789' input_text = unicode(self.input_line.text()) if all(c in valid_chars for c in input_text): if len(input_text) % 2: if self.truncate_radio.isChecked(): self.popupWindow('Improper Input Length',\ 'Input length is not a multiple of 2. Truncated. ') input_text = input_text[:-1] elif self.refuse_radio.isChecked(): self.popupWindow('Improper Input Length',\ 'Input length is not a multiple of 2. Failure to decode. ') self.output_box.clear() return #check for valid characters (A-F and 0-9) from valid_chars above output_text = str(unhexlify(input_text)) self.output_box.setText(output_text) self.updateStatus() else: self.popupWindow('Invalid Input', 'Sorry, input is not proper hexadecimal. ') self.output_box.clear() def rot13(self): try: input_text = unicode(self.input_line.text()).encode('ascii') except UnicodeEncodeError: self.popupWindow('Invalid Input', 'Sorry, input is not properly formatted. ') self.output_box.clear() return rot13 = maketrans("ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz",\ "NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm") output_text = translate(input_text, rot13) self.output_box.setText(output_text) self.updateStatus() #needs error checking... not finished def hexToDecIP(self): input_text = str(self.input_line.text()) flipped_ip = ("".join( reversed( [input_text[i:i + 2] for i in range(0, len(input_text), 2)]))) output_text = ":".join( [str(int(flipped_ip[x:x + 2], 16)) for x in range(0, 8, 2)]) self.output_box.setText(output_text)
class MainWindow(QMainWindow): about_message = """ <P>PURR ("<B>P</B>URR is <B>U</B>seful for <B>R</B>emembering <B>R</B>eductions", for those working with a stable version, or "<B>P</B>URR <B>U</B>sually <B>R</B>emembers <B>R</B>eductions", for those working with a development version, or "<B>P</B>URR <B>U</B>sed to <B>R</B>emember <B>R</B>eductions", for those working with a broken version) is a tool for automatically keeping a log of your data reduction operations. PURR will monitor your working directories for new or updated files (called "data products"), and upon seeing any, it can "pounce" -- that is, offer you the option of saving the files to a log, along with descriptive comments. It will then generate an HTML page with a pretty rendering of your log and data products.</P> """ def __init__(self, parent, hide_on_close=False): QMainWindow.__init__(self, parent) self._hide_on_close = hide_on_close # replace the BusyIndicator class with a GUI-aware one Purr.BusyIndicator = BusyIndicator self._pounce = False # we keep a small stack of previously active purrers. This makes directory changes # faster (when going back and forth between dirs) # current purrer self.purrer = None self.purrer_stack = [] # Purr pipes for receiving remote commands self.purrpipes = {} # init GUI self.setWindowTitle("PURR") self.setWindowIcon(pixmaps.purr_logo.icon()) cw = QWidget(self) self.setCentralWidget(cw) cwlo = QVBoxLayout(cw) cwlo.setContentsMargins(0, 0, 0, 0) cwlo.setMargin(5) cwlo.setSpacing(0) toplo = QHBoxLayout() cwlo.addLayout(toplo) # About dialog self._about_dialog = QMessageBox(self) self._about_dialog.setWindowTitle("About PURR") self._about_dialog.setText(self.about_message + """ <P>PURR is not watching any directories right now. You may need to restart it, and give it some directory names on the command line.</P>""") self._about_dialog.setIconPixmap(pixmaps.purr_logo.pm()) # Log viewer dialog self.viewer_dialog = HTMLViewerDialog( self, config_name="log-viewer", buttons= [(pixmaps.blue_round_reload, "Regenerate", """<P>Regenerates your log's HTML code from scratch. This can be useful if your PURR version has changed, or if there was an error of some kind the last time the files were generated.</P> """)]) self._viewer_timestamp = None self.connect(self.viewer_dialog, SIGNAL("Regenerate"), self._regenerateLog) self.connect(self.viewer_dialog, SIGNAL("viewPath"), self._viewPath) # Log title toolbar title_tb = QToolBar(cw) title_tb.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) title_tb.setIconSize(QSize(16, 16)) cwlo.addWidget(title_tb) title_label = QLabel("Purrlog title:", title_tb) title_tb.addWidget(title_label) self.title_editor = QLineEdit(title_tb) title_tb.addWidget(self.title_editor) self.connect(self.title_editor, SIGNAL("editingFinished()"), self._titleChanged) tip = """<P>This is your current log title. To rename the log, enter new name here and press Enter.</P>""" title_label.setToolTip(tip) self.title_editor.setToolTip(tip) self.wviewlog = title_tb.addAction(pixmaps.openbook.icon(), "View", self._showViewerDialog) self.wviewlog.setToolTip( "Click to see an HTML rendering of your current log.") qa = title_tb.addAction(pixmaps.purr_logo.icon(), "About...", self._about_dialog.exec_) qa.setToolTip( "<P>Click to see the About... dialog, which will tell you something about PURR.</P>" ) self.wdirframe = QFrame(cw) cwlo.addWidget(self.wdirframe) self.dirs_lo = QVBoxLayout(self.wdirframe) self.dirs_lo.setMargin(5) self.dirs_lo.setContentsMargins(5, 0, 5, 5) self.dirs_lo.setSpacing(0) self.wdirframe.setFrameStyle(QFrame.Box | QFrame.Raised) self.wdirframe.setLineWidth(1) ## Directories toolbar dirs_tb = QToolBar(self.wdirframe) dirs_tb.setToolButtonStyle(Qt.ToolButtonIconOnly) dirs_tb.setIconSize(QSize(16, 16)) self.dirs_lo.addWidget(dirs_tb) label = QLabel("Monitoring directories:", dirs_tb) self._dirs_tip = """<P>PURR can monitor your working directories for new or updated files. If there's a checkmark next to the directory name in this list, PURR is monitoring it.</P> <P>If the checkmark is grey, PURR is monitoring things unobtrusively. When a new or updated file is detected in he monitored directory, it is quietly added to the list of files in the "New entry" window, even if this window is not currently visible.</P> <P>If the checkmark is black, PURR will be more obtrusive. Whenever a new or updated file is detected, the "New entry" window will pop up automatically. This is called "pouncing", and some people find it annoying.</P> """ label.setToolTip(self._dirs_tip) label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) dirs_tb.addWidget(label) # add directory list widget self.wdirlist = DirectoryListWidget(self.wdirframe) self.wdirlist.setToolTip(self._dirs_tip) QObject.connect(self.wdirlist, SIGNAL("directoryStateChanged"), self._changeWatchedDirState) self.dirs_lo.addWidget(self.wdirlist) # self.wdirlist.setMaximumSize(1000000,64) # add directory button add = dirs_tb.addAction(pixmaps.list_add.icon(), "Add", self._showAddDirectoryDialog) add.setToolTip( "<P>Click to add another directory to be monitored.</P>") # remove directory button delbtn = dirs_tb.addAction(pixmaps.list_remove.icon(), "Remove", self.wdirlist.removeCurrent) delbtn.setEnabled(False) delbtn.setToolTip( "<P>Click to removed the currently selected directory from the list.</P>" ) QObject.connect(self.wdirlist, SIGNAL("hasSelection"), delbtn.setEnabled) # # qa = dirs_tb.addAction(pixmaps.blue_round_reload.icon(),"Rescan",self._forceRescan) # # qa.setToolTip("Click to rescan the directories for any new or updated files.") # self.wshownew = QCheckBox("show new files",dirs_tb) # dirs_tb.addWidget(self.wshownew) # self.wshownew.setCheckState(Qt.Checked) # self.wshownew.setToolTip("""<P>If this is checked, the "New entry" window will pop up automatically whenever # new or updated files are detected. If this is unchecked, the files will be added to the window quietly # and unobtrusively; you can show the window manually by clicking on the "New entry..." button below.</P>""") # self._dir_entries = {} cwlo.addSpacing(5) wlogframe = QFrame(cw) cwlo.addWidget(wlogframe) log_lo = QVBoxLayout(wlogframe) log_lo.setMargin(5) log_lo.setContentsMargins(5, 5, 5, 5) log_lo.setSpacing(0) wlogframe.setFrameStyle(QFrame.Box | QFrame.Raised) wlogframe.setLineWidth(1) # listview of log entries self.etw = LogEntryTree(cw) log_lo.addWidget(self.etw, 1) self.etw.header().setDefaultSectionSize(128) self.etw.header().setMovable(False) self.etw.setHeaderLabels(["date", "entry title", "comment"]) if hasattr(QHeaderView, 'ResizeToContents'): self.etw.header().setResizeMode(0, QHeaderView.ResizeToContents) else: self.etw.header().setResizeMode(0, QHeaderView.Custom) self.etw.header().resizeSection(0, 120) self.etw.header().setResizeMode(1, QHeaderView.Interactive) self.etw.header().setResizeMode(2, QHeaderView.Stretch) self.etw.header().show() try: self.etw.setAllColumnsShowFocus(True) except AttributeError: pass # Qt 4.2+ # self.etw.setShowToolTips(True) self.etw.setSortingEnabled(False) # self.etw.setColumnAlignment(2,Qt.AlignLeft|Qt.AlignTop) self.etw.setSelectionMode(QTreeWidget.ExtendedSelection) self.etw.setRootIsDecorated(True) self.connect(self.etw, SIGNAL("itemSelectionChanged()"), self._entrySelectionChanged) self.connect(self.etw, SIGNAL("itemActivated(QTreeWidgetItem*,int)"), self._viewEntryItem) self.connect(self.etw, SIGNAL("itemContextMenuRequested"), self._showItemContextMenu) # create popup menu for data products self._archived_dp_menu = menu = QMenu(self) self._archived_dp_menu_title = QLabel() self._archived_dp_menu_title.setMargin(5) self._archived_dp_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._archived_dp_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.editcopy.icon(), "Restore file(s) from archived copy", self._restoreItemFromArchive) menu.addAction(pixmaps.editpaste.icon(), "Copy pathname of archived copy to clipboard", self._copyItemToClipboard) self._current_item = None # create popup menu for entries self._entry_menu = menu = QMenu(self) self._entry_menu_title = QLabel() self._entry_menu_title.setMargin(5) self._entry_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._entry_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.filefind.icon(), "View this log entry", self._viewEntryItem) menu.addAction(pixmaps.editdelete.icon(), "Delete this log entry", self._deleteSelectedEntries) # buttons at bottom log_lo.addSpacing(5) btnlo = QHBoxLayout() log_lo.addLayout(btnlo) self.wnewbtn = QPushButton(pixmaps.filenew.icon(), "New entry...", cw) self.wnewbtn.setToolTip("Click to add a new log entry.") # self.wnewbtn.setFlat(True) self.wnewbtn.setEnabled(False) btnlo.addWidget(self.wnewbtn) btnlo.addSpacing(5) self.weditbtn = QPushButton(pixmaps.filefind.icon(), "View entry...", cw) self.weditbtn.setToolTip( "Click to view or edit the selected log entry/") # self.weditbtn.setFlat(True) self.weditbtn.setEnabled(False) self.connect(self.weditbtn, SIGNAL("clicked()"), self._viewEntryItem) btnlo.addWidget(self.weditbtn) btnlo.addSpacing(5) self.wdelbtn = QPushButton(pixmaps.editdelete.icon(), "Delete", cw) self.wdelbtn.setToolTip( "Click to delete the selected log entry or entries.") # self.wdelbtn.setFlat(True) self.wdelbtn.setEnabled(False) self.connect(self.wdelbtn, SIGNAL("clicked()"), self._deleteSelectedEntries) btnlo.addWidget(self.wdelbtn) # enable status line self.statusBar().show() Purr.progressMessage = self.message self._prev_msg = None # editor dialog for new entry self.new_entry_dialog = Purr.Editors.NewLogEntryDialog(self) self.connect(self.new_entry_dialog, SIGNAL("newLogEntry"), self._newLogEntry) self.connect(self.new_entry_dialog, SIGNAL("filesSelected"), self._addDPFiles) self.connect(self.wnewbtn, SIGNAL("clicked()"), self.new_entry_dialog.show) self.connect(self.new_entry_dialog, SIGNAL("shown"), self._checkPounceStatus) # entry viewer dialog self.view_entry_dialog = Purr.Editors.ExistingLogEntryDialog(self) self.connect(self.view_entry_dialog, SIGNAL("previous()"), self._viewPrevEntry) self.connect(self.view_entry_dialog, SIGNAL("next()"), self._viewNextEntry) self.connect(self.view_entry_dialog, SIGNAL("viewPath"), self._viewPath) self.connect(self.view_entry_dialog, SIGNAL("filesSelected"), self._addDPFilesToOldEntry) self.connect(self.view_entry_dialog, SIGNAL("entryChanged"), self._entryChanged) # saving a data product to an older entry will automatically drop it from the # new entry dialog self.connect(self.view_entry_dialog, SIGNAL("creatingDataProduct"), self.new_entry_dialog.dropDataProducts) # resize selves width = Config.getint('main-window-width', 512) height = Config.getint('main-window-height', 512) self.resize(QSize(width, height)) # create timer for pouncing self._timer = QTimer(self) self.connect(self._timer, SIGNAL("timeout()"), self._rescan) # create dict mapping index.html paths to entry numbers self._index_paths = {} def resizeEvent(self, ev): QMainWindow.resizeEvent(self, ev) sz = ev.size() Config.set('main-window-width', sz.width()) Config.set('main-window-height', sz.height()) def closeEvent(self, ev): if self._hide_on_close: ev.ignore() self.hide() self.new_entry_dialog.hide() else: if self.purrer: self.purrer.detach() return QMainWindow.closeEvent(self, ev) def message(self, msg, ms=2000, sub=False): if sub: if self._prev_msg: msg = ": ".join((self._prev_msg, msg)) else: self._prev_msg = msg self.statusBar().showMessage(msg, ms) QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) def _changeWatchedDirState(self, pathname, watching): self.purrer.setWatchingState(pathname, watching) # update dialogs if dir list has changed if watching == Purr.REMOVED: self.purrpipes.pop(pathname) dirs = [path for path, state in self.purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) pass def _showAddDirectoryDialog(self): dd = str( QFileDialog.getExistingDirectory( self, "PURR: Add a directory to monitor")).strip() if dd: # adds a watched directory. Default initial setting of 'watching' is POUNCE if all # directories are in POUNCE state, or WATCHED otherwise. watching = max( Purr.WATCHED, min([ state for path, state in self.purrer.watchedDirectories() ] or [Purr.WATCHED])) self.purrer.addWatchedDirectory(dd, watching) self.purrpipes[dd] = Purr.Pipe.open(dd) self.wdirlist.add(dd, watching) # update dialogs since dir list has changed dirs = [path for path, state in self.purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) def detachPurrlog(self): self.wdirlist.clear() self.purrer and self.purrer.detach() self.purrer = None def hasPurrlog(self): return bool(self.purrer) def attachPurrlog(self, purrlog, watchdirs=[]): """Attaches Purr to the given purrlog directory. Arguments are passed to Purrer object as is.""" # check purrer stack for a Purrer already watching this directory dprint(1, "attaching to purrlog", purrlog) for i, purrer in enumerate(self.purrer_stack): if os.path.samefile(purrer.logdir, purrlog): dprint(1, "Purrer object found on stack (#%d),reusing\n", i) # found? move to front of stack self.purrer_stack.pop(i) self.purrer_stack.insert(0, purrer) # update purrer with watched directories, in case they have changed for dd in (watchdirs or []): purrer.addWatchedDirectory(dd, watching=None) break # no purrer found, make a new one else: dprint(1, "creating new Purrer object") try: purrer = Purr.Purrer(purrlog, watchdirs) except Purr.Purrer.LockedError as err: # check that we could attach, display message if not QMessageBox.warning( self, "Catfight!", """<P><NOBR>It appears that another PURR process (%s)</NOBR> is already attached to <tt>%s</tt>, so we're not allowed to touch it. You should exit the other PURR process first.</P>""" % (err.args[0], os.path.abspath(purrlog)), QMessageBox.Ok, 0) return False except Purr.Purrer.LockFailError as err: QMessageBox.warning( self, "Failed to obtain lock", """<P><NOBR>PURR was unable to obtain a lock</NOBR> on directory <tt>%s</tt> (error was "%s"). The most likely cause is insufficient permissions.</P>""" % (os.path.abspath(purrlog), err.args[0]), QMessageBox.Ok, 0) return False self.purrer_stack.insert(0, purrer) # discard end of stack self.purrer_stack = self.purrer_stack[:3] # attach signals self.connect(purrer, SIGNAL("disappearedFile"), self.new_entry_dialog.dropDataProducts) self.connect(purrer, SIGNAL("disappearedFile"), self.view_entry_dialog.dropDataProducts) # have we changed the current purrer? Update our state then # reopen Purr pipes self.purrpipes = {} for dd, state in purrer.watchedDirectories(): self.purrpipes[dd] = Purr.Pipe.open(dd) if purrer is not self.purrer: self.message("Attached to %s" % purrer.logdir, ms=10000) dprint(1, "current Purrer changed, updating state") # set window title path = Kittens.utils.collapseuser(os.path.join(purrer.logdir, '')) self.setWindowTitle("PURR - %s" % path) # other init self.purrer = purrer self.new_entry_dialog.hide() self.new_entry_dialog.reset() dirs = [path for path, state in purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.hide() self.viewer_dialog.hide() self._viewing_ientry = None self._setEntries(self.purrer.getLogEntries()) # print self._index_paths self._viewer_timestamp = None self._updateViewer() self._updateNames() # update directory widgets self.wdirlist.clear() for pathname, state in purrer.watchedDirectories(): self.wdirlist.add(pathname, state) # Reset _pounce to false -- this will cause checkPounceStatus() into a rescan self._pounce = False self._checkPounceStatus() return True def setLogTitle(self, title): if self.purrer: if title != self.purrer.logtitle: self.purrer.setLogTitle(title) self._updateViewer() self._updateNames() def _updateNames(self): self.wnewbtn.setEnabled(True) self.wviewlog.setEnabled(True) self._about_dialog.setText(self.about_message + """ <P>Your current log resides in:<PRE> <tt>%s</tt></PRE>To see your log in all its HTML-rendered glory, point your browser to <tt>index.html</tt> therein, or use the handy "View" button provided by PURR.</P> <P>Your current working directories are:</P> <P>%s</P> """ % (self.purrer.logdir, "".join([ "<PRE> <tt>%s</tt></PRE>" % name for name, state in self.purrer.watchedDirectories() ]))) title = self.purrer.logtitle or "Unnamed log" self.title_editor.setText(title) self.viewer_dialog.setWindowTitle(title) def _showViewerDialog(self): self._updateViewer(True) self.viewer_dialog.show() @staticmethod def fileModTime(path): try: return os.path.getmtime(path) except: return None def _updateViewer(self, force=False): """Updates the viewer dialog. If dialog is not visible and force=False, does nothing. Otherwise, checks the mtime of the current purrer index.html file against self._viewer_timestamp. If it is newer, reloads it. """ if not force and not self.viewer_dialog.isVisible(): return # default text if nothing is found path = self.purrer.indexfile mtime = self.fileModTime(path) # return if file is older than our content if mtime and mtime <= (self._viewer_timestamp or 0): return busy = BusyIndicator() self.viewer_dialog.setDocument( path, empty="<P>Nothing in the log yet. Try adding some log entries.</P>" ) self.viewer_dialog.reload() self.viewer_dialog.setLabel( """<P>Below is your full HTML-rendered log. Note that this is only a bare-bones viewer, so only a limited set of links will work. For a fully-functional view, use a proper HTML browser to look at the index file residing here:<BR> <tt>%s</tt></P> """ % self.purrer.indexfile) self._viewer_timestamp = mtime def _setEntries(self, entries): self.etw.clear() item = None self._index_paths = {} self._index_paths[os.path.abspath(self.purrer.indexfile)] = -1 for i, entry in enumerate(entries): item = self._addEntryItem(entry, i, item) self._index_paths[os.path.abspath(entry.index_file)] = i self.etw.resizeColumnToContents(0) def _titleChanged(self): self.setLogTitle(str(self.title_editor.text())) def _checkPounceStatus(self): ## pounce = bool([ entry for entry in self._dir_entries.itervalues() if entry.watching ]) pounce = bool([ path for path, state in self.purrer.watchedDirectories() if state >= Purr.WATCHED ]) # rescan, if going from not-pounce to pounce if pounce and not self._pounce: self._rescan() self._pounce = pounce # start timer -- we need it running to check the purr pipe, anyway self._timer.start(2000) def _forceRescan(self): if not self.purrer: self.attachDirectory('.') self._rescan(force=True) def _rescan(self, force=False): if not self.purrer: return # if pounce is on, tell the Purrer to rescan directories if self._pounce or force: dps = self.purrer.rescan() if dps: filenames = [dp.filename for dp in dps] dprint(2, "new data products:", filenames) self.message("Pounced on " + ", ".join(filenames)) if self.new_entry_dialog.addDataProducts(dps): dprint(2, "showing dialog") self.new_entry_dialog.show() # else read stuff from pipe for pipe in self.purrpipes.values(): do_show = False for command, show, content in pipe.read(): if command == "title": self.new_entry_dialog.suggestTitle(content) elif command == "comment": self.new_entry_dialog.addComment(content) elif command == "pounce": self.new_entry_dialog.addDataProducts( self.purrer.makeDataProducts([(content, not show)], unbanish=True)) else: print("Unknown command received from Purr pipe: ", command) continue do_show = do_show or show if do_show: self.new_entry_dialog.show() def _addDPFiles(self, *files): """callback to add DPs corresponding to files.""" # quiet flag is always true self.new_entry_dialog.addDataProducts( self.purrer.makeDataProducts([(file, True) for file in files], unbanish=True, unignore=True)) def _addDPFilesToOldEntry(self, *files): """callback to add DPs corresponding to files.""" # quiet flag is always true self.view_entry_dialog.addDataProducts( self.purrer.makeDataProducts([(file, True) for file in files], unbanish=True, unignore=True)) def _entrySelectionChanged(self): selected = [ item for item in self.etw.iterator(self.etw.Iterator.Selected) if item._ientry is not None ] self.weditbtn.setEnabled(len(selected) == 1) self.wdelbtn.setEnabled(bool(selected)) def _viewEntryItem(self, item=None, *dum): """Pops up the viewer dialog for the entry associated with the given item. If 'item' is None, looks for a selected item in the listview. The dum arguments are for connecting this to QTreeWidget signals such as doubleClicked(). """ # if item not set, look for selected items in listview. Only 1 must be selected. select = True if item is None: selected = [ item for item in self.etw.iterator(self.etw.Iterator.Selected) if item._ientry is not None ] if len(selected) != 1: return item = selected[0] select = False # already selected else: # make sure item is open -- the click will cause it to close self.etw.expandItem(item) # show dialog ientry = getattr(item, '_ientry', None) if ientry is not None: self._viewEntryNumber(ientry, select=select) def _viewEntryNumber(self, ientry, select=True): """views entry #ientry. Also selects entry in listview if select=True""" # pass entry to viewer dialog self._viewing_ientry = ientry entry = self.purrer.entries[ientry] busy = BusyIndicator() self.view_entry_dialog.viewEntry( entry, prev=ientry > 0 and self.purrer.entries[ientry - 1], next=ientry < len(self.purrer.entries) - 1 and self.purrer.entries[ientry + 1]) self.view_entry_dialog.show() # select entry in listview if select: self.etw.clearSelection() self.etw.setItemSelected(self.etw.topLevelItem(ientry), True) def _viewPrevEntry(self): if self._viewing_ientry is not None and self._viewing_ientry > 0: self._viewEntryNumber(self._viewing_ientry - 1) def _viewNextEntry(self): if self._viewing_ientry is not None and self._viewing_ientry < len( self.purrer.entries) - 1: self._viewEntryNumber(self._viewing_ientry + 1) def _viewPath(self, path): num = self._index_paths.get(os.path.abspath(path), None) if num is None: return elif num == -1: self.view_entry_dialog.hide() self._showViewerDialog() else: self._viewEntryNumber(num) def _showItemContextMenu(self, item, point, col): """Callback for contextMenuRequested() signal. Pops up item menu, if defined""" menu = getattr(item, '_menu', None) if menu: settitle = getattr(item, '_set_menu_title', None) if settitle: settitle() # self._current_item tells callbacks what item the menu was referring to point = self.etw.mapToGlobal(point) self._current_item = item self.etw.clearSelection() self.etw.setItemSelected(item, True) menu.exec_(point) else: self._current_item = None def _copyItemToClipboard(self): """Callback for item menu.""" if self._current_item is None: return dp = getattr(self._current_item, '_dp', None) if dp and dp.archived: path = dp.fullpath.replace(" ", "\\ ") QApplication.clipboard().setText(path, QClipboard.Clipboard) QApplication.clipboard().setText(path, QClipboard.Selection) def _restoreItemFromArchive(self): """Callback for item menu.""" if self._current_item is None: return dp = getattr(self._current_item, '_dp', None) if dp and dp.archived: dp.restore_from_archive(parent=self) def _deleteSelectedEntries(self): remaining_entries = [] del_entries = list(self.etw.iterator(self.etw.Iterator.Selected)) remaining_entries = list( self.etw.iterator(self.etw.Iterator.Unselected)) if not del_entries: return hide_viewer = bool([ item for item in del_entries if self._viewing_ientry == item._ientry ]) del_entries = [ self.purrer.entries[self.etw.indexOfTopLevelItem(item)] for item in del_entries ] remaining_entries = [ self.purrer.entries[self.etw.indexOfTopLevelItem(item)] for item in remaining_entries ] # ask for confirmation if len(del_entries) == 1: msg = """<P><NOBR>Permanently delete the log entry</NOBR> "%s"?</P>""" % del_entries[ 0].title if del_entries[0].dps: msg += """<P>%d data product(s) saved with this entry will be deleted as well.</P>""" % len( del_entries[0].dps) else: msg = """<P>Permanently delete the %d selected log entries?</P>""" % len( del_entries) ndp = 0 for entry in del_entries: ndp += len([dp for dp in entry.dps if not dp.ignored]) if ndp: msg += """<P>%d data product(s) saved with these entries will be deleted as well.</P>""" % ndp if QMessageBox.warning(self, "Deleting log entries", msg, QMessageBox.Yes, QMessageBox.No) != QMessageBox.Yes: return if hide_viewer: self.view_entry_dialog.hide() # reset entries in purrer and in our log window self._setEntries(remaining_entries) self.purrer.deleteLogEntries(del_entries) # self.purrer.setLogEntries(remaining_entries) # log will have changed, so update the viewer self._updateViewer() # delete entry files for entry in del_entries: entry.remove_directory() def _addEntryItem(self, entry, number, after): item = entry.tw_item = QTreeWidgetItem(self.etw, after) timelabel = self._make_time_label(entry.timestamp) item.setText(0, timelabel) item.setText(1, " " + (entry.title or "")) item.setToolTip(1, entry.title) if entry.comment: item.setText(2, " " + entry.comment.split('\n')[0]) item.setToolTip(2, "<P>" + entry.comment.replace("<", "<").replace(">", ">"). \ replace("\n\n", "</P><P>").replace("\n", "</P><P>") + "</P>") item._ientry = number item._dp = None item._menu = self._entry_menu item._set_menu_title = lambda: self._entry_menu_title.setText( '"%s"' % entry.title) # now make subitems for DPs subitem = None for dp in entry.dps: if not dp.ignored: subitem = self._addDPSubItem(dp, item, subitem) self.etw.collapseItem(item) self.etw.header().headerDataChanged(Qt.Horizontal, 0, 2) return item def _addDPSubItem(self, dp, parent, after): item = QTreeWidgetItem(parent, after) item.setText(1, dp.filename) item.setToolTip(1, dp.filename) item.setText(2, dp.comment or "") item.setToolTip(2, dp.comment or "") item._ientry = None item._dp = dp item._menu = self._archived_dp_menu item._set_menu_title = lambda: self._archived_dp_menu_title.setText( os.path.basename(dp.filename)) return item def _make_time_label(self, timestamp): return time.strftime("%b %d %H:%M", time.localtime(timestamp)) def _newLogEntry(self, entry): """This is called when a new log entry is created""" # add entry to purrer self.purrer.addLogEntry(entry) # add entry to listview if it is not an ignored entry # (ignored entries only carry information about DPs to be ignored) if not entry.ignore: if self.etw.topLevelItemCount(): lastitem = self.etw.topLevelItem(self.etw.topLevelItemCount() - 1) else: lastitem = None self._addEntryItem(entry, len(self.purrer.entries) - 1, lastitem) self._index_paths[os.path.abspath( entry.index_file)] = len(self.purrer.entries) - 1 # log will have changed, so update the viewer if not entry.ignore: self._updateViewer() self.show() def _entryChanged(self, entry): """This is called when a log entry is changed""" # resave the log self.purrer.save() # redo entry item if entry.tw_item: number = entry.tw_item._ientry entry.tw_item = None self.etw.takeTopLevelItem(number) if number: after = self.etw.topLevelItem(number - 1) else: after = None self._addEntryItem(entry, number, after) # log will have changed, so update the viewer self._updateViewer() def _regenerateLog(self): if QMessageBox.question( self.viewer_dialog, "Regenerate log", """<P><NOBR>Do you really want to regenerate the entire</NOBR> log? This can be a time-consuming operation.</P>""", QMessageBox.Yes, QMessageBox.No) != QMessageBox.Yes: return self.purrer.save(refresh=True) self._updateViewer()
def step_dialog(parent, title, msg, det_msg=''): d = QMessageBox(parent) d.setWindowTitle(title) d.setText(msg) d.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) return d.exec_() & QMessageBox.Cancel
class MessageWindow: def __init__(self, title, text, type="ok", default=None, customButtons=None, customIcon=None, run=True, destroyAfterRun=True, detailed=False, longText=""): self.rc = None self.dialog = None self.msgBox = QMessageBox() self.doCustom = False self.customButtons = customButtons icon = None buttons = None if type == 'ok': buttons = QMessageBox.Ok icon = "question" elif type == 'error': icon = "error" buttons = QMessageBox.Ok elif type == 'warning': icon = "warning" buttons = QMessageBox.Ok elif type == 'okcancel': icon = "question" buttons = QMessageBox.Ok | QMessageBox.Cancel elif type == 'question': icon = "question" buttons = QMessageBox.Ok | QMessageBox.Cancel elif type == 'yesno': icon = "question" buttons = QMessageBox.Yes | QMessageBox.No elif type == 'custom': self.doCustom = True if customIcon: icon = customIcon else: icon = "question" text = "<qt>%s</qt>" % text.replace("\n", " ") self.msgBox.setText(text) if detailed: self.msgBox.setDetailedText(unicode(longText)) if self.doCustom: button = None for index, text in enumerate(self.customButtons): button = self.msgBox.addButton(text, QMessageBox.ActionRole) if default is not None and default == index: self.msgBox.setDefaultButton(button) else: self.msgBox.setStandardButtons(buttons) if default == "no": default = QMessageBox.No elif default == "yes": default = QMessageBox.Yes elif default == "ok": default = QMessageBox.Ok else: default = None self.msgBox.setDefaultButton(default) self.dialog = Dialog(_(title), self.msgBox, closeButton=False, isDialog=True, icon=icon) self.dialog.resize(QSize(0, 0)) if run: self.run(destroyAfterRun) def run(self, destroyAfterRun=True): self.rc = self.dialog.exec_() if self.msgBox.clickedButton(): if not self.doCustom: if self.msgBox.clickedButton().text() in [_("Ok"), _("Yes")]: self.rc = 1 elif self.msgBox.clickedButton().text() in [ _("Cancel"), _("No") ]: self.rc = 0 else: if self.msgBox.clickedButton().text() in self.customButtons: self.rc = self.customButtons.index( self.msgBox.clickedButton().text()) if destroyAfterRun: self.dialog = None return self.rc
class MessageWindow: def __init__(self, title, text, type="ok", default=None, customButtons =None, customIcon=None, run=True, destroyAfterRun=True, detailed=False, longText=""): self.rc = None self.dialog = None self.msgBox = QMessageBox() self.doCustom = False self.customButtons = customButtons icon = None buttons = None if type == 'ok': buttons = QMessageBox.Ok icon = "question" elif type == 'error': icon = "error" buttons = QMessageBox.Ok elif type == 'warning': icon = "warning" buttons = QMessageBox.Ok elif type == 'okcancel': icon = "question" buttons = QMessageBox.Ok | QMessageBox.Cancel elif type == 'question': icon = "question" buttons = QMessageBox.Ok | QMessageBox.Cancel elif type == 'yesno': icon = "question" buttons = QMessageBox.Yes | QMessageBox.No elif type == 'custom': self.doCustom = True if customIcon: icon = customIcon else: icon = "question" text = "<qt>%s</qt>" % text.replace("\n", " ") self.msgBox.setText(text) if detailed: self.msgBox.setDetailedText(unicode(longText)) if self.doCustom: button = None for index, text in enumerate(self.customButtons): button = self.msgBox.addButton(text, QMessageBox.ActionRole) if default is not None and default == index: self.msgBox.setDefaultButton(button) else: self.msgBox.setStandardButtons(buttons) if default == "no": default = QMessageBox.No elif default == "yes": default = QMessageBox.Yes elif default == "ok": default = QMessageBox.Ok else: default = None self.msgBox.setDefaultButton(default) self.dialog = Dialog(_(title), self.msgBox, closeButton=False, isDialog=True, icon=icon) self.dialog.resize(QSize(0,0)) if run: self.run(destroyAfterRun) def run(self, destroyAfterRun=True): self.rc = self.dialog.exec_() if self.msgBox.clickedButton(): if not self.doCustom: if self.msgBox.clickedButton().text() in [_("Ok"), _("Yes")]: self.rc = 1 elif self.msgBox.clickedButton().text() in [_("Cancel"), _("No")]: self.rc = 0 else: if self.msgBox.clickedButton().text() in self.customButtons: self.rc = self.customButtons.index(self.msgBox.clickedButton().text()) if destroyAfterRun: self.dialog = None return self.rc
class MainWindow(QMainWindow): about_message = """ <P>PURR ("<B>P</B>URR is <B>U</B>seful for <B>R</B>emembering <B>R</B>eductions", for those working with a stable version, or "<B>P</B>URR <B>U</B>sually <B>R</B>emembers <B>R</B>eductions", for those working with a development version, or "<B>P</B>URR <B>U</B>sed to <B>R</B>emember <B>R</B>eductions", for those working with a broken version) is a tool for automatically keeping a log of your data reduction operations. PURR will monitor your working directories for new or updated files (called "data products"), and upon seeing any, it can "pounce" -- that is, offer you the option of saving the files to a log, along with descriptive comments. It will then generate an HTML page with a pretty rendering of your log and data products.</P> """ def __init__(self, parent, hide_on_close=False): QMainWindow.__init__(self, parent) self._hide_on_close = hide_on_close # replace the BusyIndicator class with a GUI-aware one Purr.BusyIndicator = BusyIndicator self._pounce = False # we keep a small stack of previously active purrers. This makes directory changes # faster (when going back and forth between dirs) # current purrer self.purrer = None self.purrer_stack = [] # Purr pipes for receiving remote commands self.purrpipes = {} # init GUI self.setWindowTitle("PURR") self.setWindowIcon(pixmaps.purr_logo.icon()) cw = QWidget(self) self.setCentralWidget(cw) cwlo = QVBoxLayout(cw) cwlo.setContentsMargins(0, 0, 0, 0) cwlo.setMargin(5) cwlo.setSpacing(0) toplo = QHBoxLayout(); cwlo.addLayout(toplo) # About dialog self._about_dialog = QMessageBox(self) self._about_dialog.setWindowTitle("About PURR") self._about_dialog.setText(self.about_message + """ <P>PURR is not watching any directories right now. You may need to restart it, and give it some directory names on the command line.</P>""") self._about_dialog.setIconPixmap(pixmaps.purr_logo.pm()) # Log viewer dialog self.viewer_dialog = HTMLViewerDialog(self, config_name="log-viewer", buttons=[(pixmaps.blue_round_reload, "Regenerate", """<P>Regenerates your log's HTML code from scratch. This can be useful if your PURR version has changed, or if there was an error of some kind the last time the files were generated.</P> """)]) self._viewer_timestamp = None self.connect(self.viewer_dialog, SIGNAL("Regenerate"), self._regenerateLog) self.connect(self.viewer_dialog, SIGNAL("viewPath"), self._viewPath) # Log title toolbar title_tb = QToolBar(cw) title_tb.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) title_tb.setIconSize(QSize(16, 16)) cwlo.addWidget(title_tb) title_label = QLabel("Purrlog title:", title_tb) title_tb.addWidget(title_label) self.title_editor = QLineEdit(title_tb) title_tb.addWidget(self.title_editor) self.connect(self.title_editor, SIGNAL("editingFinished()"), self._titleChanged) tip = """<P>This is your current log title. To rename the log, enter new name here and press Enter.</P>""" title_label.setToolTip(tip) self.title_editor.setToolTip(tip) self.wviewlog = title_tb.addAction(pixmaps.openbook.icon(), "View", self._showViewerDialog) self.wviewlog.setToolTip("Click to see an HTML rendering of your current log.") qa = title_tb.addAction(pixmaps.purr_logo.icon(), "About...", self._about_dialog.exec_) qa.setToolTip("<P>Click to see the About... dialog, which will tell you something about PURR.</P>") self.wdirframe = QFrame(cw) cwlo.addWidget(self.wdirframe) self.dirs_lo = QVBoxLayout(self.wdirframe) self.dirs_lo.setMargin(5) self.dirs_lo.setContentsMargins(5, 0, 5, 5) self.dirs_lo.setSpacing(0) self.wdirframe.setFrameStyle(QFrame.Box | QFrame.Raised) self.wdirframe.setLineWidth(1) ## Directories toolbar dirs_tb = QToolBar(self.wdirframe) dirs_tb.setToolButtonStyle(Qt.ToolButtonIconOnly) dirs_tb.setIconSize(QSize(16, 16)) self.dirs_lo.addWidget(dirs_tb) label = QLabel("Monitoring directories:", dirs_tb) self._dirs_tip = """<P>PURR can monitor your working directories for new or updated files. If there's a checkmark next to the directory name in this list, PURR is monitoring it.</P> <P>If the checkmark is grey, PURR is monitoring things unobtrusively. When a new or updated file is detected in he monitored directory, it is quietly added to the list of files in the "New entry" window, even if this window is not currently visible.</P> <P>If the checkmark is black, PURR will be more obtrusive. Whenever a new or updated file is detected, the "New entry" window will pop up automatically. This is called "pouncing", and some people find it annoying.</P> """ label.setToolTip(self._dirs_tip) label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) dirs_tb.addWidget(label) # add directory list widget self.wdirlist = DirectoryListWidget(self.wdirframe) self.wdirlist.setToolTip(self._dirs_tip) QObject.connect(self.wdirlist, SIGNAL("directoryStateChanged"), self._changeWatchedDirState) self.dirs_lo.addWidget(self.wdirlist) # self.wdirlist.setMaximumSize(1000000,64) # add directory button add = dirs_tb.addAction(pixmaps.list_add.icon(), "Add", self._showAddDirectoryDialog) add.setToolTip("<P>Click to add another directory to be monitored.</P>") # remove directory button delbtn = dirs_tb.addAction(pixmaps.list_remove.icon(), "Remove", self.wdirlist.removeCurrent) delbtn.setEnabled(False) delbtn.setToolTip("<P>Click to removed the currently selected directory from the list.</P>") QObject.connect(self.wdirlist, SIGNAL("hasSelection"), delbtn.setEnabled) # # qa = dirs_tb.addAction(pixmaps.blue_round_reload.icon(),"Rescan",self._forceRescan) # # qa.setToolTip("Click to rescan the directories for any new or updated files.") # self.wshownew = QCheckBox("show new files",dirs_tb) # dirs_tb.addWidget(self.wshownew) # self.wshownew.setCheckState(Qt.Checked) # self.wshownew.setToolTip("""<P>If this is checked, the "New entry" window will pop up automatically whenever # new or updated files are detected. If this is unchecked, the files will be added to the window quietly # and unobtrusively; you can show the window manually by clicking on the "New entry..." button below.</P>""") # self._dir_entries = {} cwlo.addSpacing(5) wlogframe = QFrame(cw) cwlo.addWidget(wlogframe) log_lo = QVBoxLayout(wlogframe) log_lo.setMargin(5) log_lo.setContentsMargins(5, 5, 5, 5) log_lo.setSpacing(0) wlogframe.setFrameStyle(QFrame.Box | QFrame.Raised) wlogframe.setLineWidth(1) # listview of log entries self.etw = LogEntryTree(cw) log_lo.addWidget(self.etw, 1) self.etw.header().setDefaultSectionSize(128) self.etw.header().setMovable(False) self.etw.setHeaderLabels(["date", "entry title", "comment"]) if hasattr(QHeaderView, 'ResizeToContents'): self.etw.header().setResizeMode(0, QHeaderView.ResizeToContents) else: self.etw.header().setResizeMode(0, QHeaderView.Custom) self.etw.header().resizeSection(0, 120) self.etw.header().setResizeMode(1, QHeaderView.Interactive) self.etw.header().setResizeMode(2, QHeaderView.Stretch) self.etw.header().show() try: self.etw.setAllColumnsShowFocus(True) except AttributeError: pass; # Qt 4.2+ # self.etw.setShowToolTips(True) self.etw.setSortingEnabled(False) # self.etw.setColumnAlignment(2,Qt.AlignLeft|Qt.AlignTop) self.etw.setSelectionMode(QTreeWidget.ExtendedSelection) self.etw.setRootIsDecorated(True) self.connect(self.etw, SIGNAL("itemSelectionChanged()"), self._entrySelectionChanged) self.connect(self.etw, SIGNAL("itemActivated(QTreeWidgetItem*,int)"), self._viewEntryItem) self.connect(self.etw, SIGNAL("itemContextMenuRequested"), self._showItemContextMenu) # create popup menu for data products self._archived_dp_menu = menu = QMenu(self) self._archived_dp_menu_title = QLabel() self._archived_dp_menu_title.setMargin(5) self._archived_dp_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._archived_dp_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.editcopy.icon(), "Restore file(s) from archived copy", self._restoreItemFromArchive) menu.addAction(pixmaps.editpaste.icon(), "Copy pathname of archived copy to clipboard", self._copyItemToClipboard) self._current_item = None # create popup menu for entries self._entry_menu = menu = QMenu(self) self._entry_menu_title = QLabel() self._entry_menu_title.setMargin(5) self._entry_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._entry_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.filefind.icon(), "View this log entry", self._viewEntryItem) menu.addAction(pixmaps.editdelete.icon(), "Delete this log entry", self._deleteSelectedEntries) # buttons at bottom log_lo.addSpacing(5) btnlo = QHBoxLayout() log_lo.addLayout(btnlo) self.wnewbtn = QPushButton(pixmaps.filenew.icon(), "New entry...", cw) self.wnewbtn.setToolTip("Click to add a new log entry.") # self.wnewbtn.setFlat(True) self.wnewbtn.setEnabled(False) btnlo.addWidget(self.wnewbtn) btnlo.addSpacing(5) self.weditbtn = QPushButton(pixmaps.filefind.icon(), "View entry...", cw) self.weditbtn.setToolTip("Click to view or edit the selected log entry/") # self.weditbtn.setFlat(True) self.weditbtn.setEnabled(False) self.connect(self.weditbtn, SIGNAL("clicked()"), self._viewEntryItem) btnlo.addWidget(self.weditbtn) btnlo.addSpacing(5) self.wdelbtn = QPushButton(pixmaps.editdelete.icon(), "Delete", cw) self.wdelbtn.setToolTip("Click to delete the selected log entry or entries.") # self.wdelbtn.setFlat(True) self.wdelbtn.setEnabled(False) self.connect(self.wdelbtn, SIGNAL("clicked()"), self._deleteSelectedEntries) btnlo.addWidget(self.wdelbtn) # enable status line self.statusBar().show() Purr.progressMessage = self.message self._prev_msg = None # editor dialog for new entry self.new_entry_dialog = Purr.Editors.NewLogEntryDialog(self) self.connect(self.new_entry_dialog, SIGNAL("newLogEntry"), self._newLogEntry) self.connect(self.new_entry_dialog, SIGNAL("filesSelected"), self._addDPFiles) self.connect(self.wnewbtn, SIGNAL("clicked()"), self.new_entry_dialog.show) self.connect(self.new_entry_dialog, SIGNAL("shown"), self._checkPounceStatus) # entry viewer dialog self.view_entry_dialog = Purr.Editors.ExistingLogEntryDialog(self) self.connect(self.view_entry_dialog, SIGNAL("previous()"), self._viewPrevEntry) self.connect(self.view_entry_dialog, SIGNAL("next()"), self._viewNextEntry) self.connect(self.view_entry_dialog, SIGNAL("viewPath"), self._viewPath) self.connect(self.view_entry_dialog, SIGNAL("filesSelected"), self._addDPFilesToOldEntry) self.connect(self.view_entry_dialog, SIGNAL("entryChanged"), self._entryChanged) # saving a data product to an older entry will automatically drop it from the # new entry dialog self.connect(self.view_entry_dialog, SIGNAL("creatingDataProduct"), self.new_entry_dialog.dropDataProducts) # resize selves width = Config.getint('main-window-width', 512) height = Config.getint('main-window-height', 512) self.resize(QSize(width, height)) # create timer for pouncing self._timer = QTimer(self) self.connect(self._timer, SIGNAL("timeout()"), self._rescan) # create dict mapping index.html paths to entry numbers self._index_paths = {} def resizeEvent(self, ev): QMainWindow.resizeEvent(self, ev) sz = ev.size() Config.set('main-window-width', sz.width()) Config.set('main-window-height', sz.height()) def closeEvent(self, ev): if self._hide_on_close: ev.ignore() self.hide() self.new_entry_dialog.hide() else: if self.purrer: self.purrer.detach() return QMainWindow.closeEvent(self, ev) def message(self, msg, ms=2000, sub=False): if sub: if self._prev_msg: msg = ": ".join((self._prev_msg, msg)) else: self._prev_msg = msg self.statusBar().showMessage(msg, ms) QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) def _changeWatchedDirState(self, pathname, watching): self.purrer.setWatchingState(pathname, watching) # update dialogs if dir list has changed if watching == Purr.REMOVED: self.purrpipes.pop(pathname) dirs = [path for path, state in self.purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) pass def _showAddDirectoryDialog(self): dd = str(QFileDialog.getExistingDirectory(self, "PURR: Add a directory to monitor")).strip() if dd: # adds a watched directory. Default initial setting of 'watching' is POUNCE if all # directories are in POUNCE state, or WATCHED otherwise. watching = max(Purr.WATCHED, min([state for path, state in self.purrer.watchedDirectories()] or [Purr.WATCHED])) self.purrer.addWatchedDirectory(dd, watching) self.purrpipes[dd] = Purr.Pipe.open(dd) self.wdirlist.add(dd, watching) # update dialogs since dir list has changed dirs = [path for path, state in self.purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) def detachPurrlog(self): self.wdirlist.clear() self.purrer and self.purrer.detach() self.purrer = None def hasPurrlog(self): return bool(self.purrer) def attachPurrlog(self, purrlog, watchdirs=[]): """Attaches Purr to the given purrlog directory. Arguments are passed to Purrer object as is.""" # check purrer stack for a Purrer already watching this directory dprint(1, "attaching to purrlog", purrlog) for i, purrer in enumerate(self.purrer_stack): if os.path.samefile(purrer.logdir, purrlog): dprint(1, "Purrer object found on stack (#%d),reusing\n", i) # found? move to front of stack self.purrer_stack.pop(i) self.purrer_stack.insert(0, purrer) # update purrer with watched directories, in case they have changed for dd in (watchdirs or []): purrer.addWatchedDirectory(dd, watching=None) break # no purrer found, make a new one else: dprint(1, "creating new Purrer object") try: purrer = Purr.Purrer(purrlog, watchdirs) except Purr.Purrer.LockedError as err: # check that we could attach, display message if not QMessageBox.warning(self, "Catfight!", """<P><NOBR>It appears that another PURR process (%s)</NOBR> is already attached to <tt>%s</tt>, so we're not allowed to touch it. You should exit the other PURR process first.</P>""" % (err.args[0], os.path.abspath(purrlog)), QMessageBox.Ok, 0) return False except Purr.Purrer.LockFailError as err: QMessageBox.warning(self, "Failed to obtain lock", """<P><NOBR>PURR was unable to obtain a lock</NOBR> on directory <tt>%s</tt> (error was "%s"). The most likely cause is insufficient permissions.</P>""" % ( os.path.abspath(purrlog), err.args[0]), QMessageBox.Ok, 0) return False self.purrer_stack.insert(0, purrer) # discard end of stack self.purrer_stack = self.purrer_stack[:3] # attach signals self.connect(purrer, SIGNAL("disappearedFile"), self.new_entry_dialog.dropDataProducts) self.connect(purrer, SIGNAL("disappearedFile"), self.view_entry_dialog.dropDataProducts) # have we changed the current purrer? Update our state then # reopen Purr pipes self.purrpipes = {} for dd, state in purrer.watchedDirectories(): self.purrpipes[dd] = Purr.Pipe.open(dd) if purrer is not self.purrer: self.message("Attached to %s" % purrer.logdir, ms=10000) dprint(1, "current Purrer changed, updating state") # set window title path = Kittens.utils.collapseuser(os.path.join(purrer.logdir, '')) self.setWindowTitle("PURR - %s" % path) # other init self.purrer = purrer self.new_entry_dialog.hide() self.new_entry_dialog.reset() dirs = [path for path, state in purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.hide() self.viewer_dialog.hide() self._viewing_ientry = None self._setEntries(self.purrer.getLogEntries()) # print self._index_paths self._viewer_timestamp = None self._updateViewer() self._updateNames() # update directory widgets self.wdirlist.clear() for pathname, state in purrer.watchedDirectories(): self.wdirlist.add(pathname, state) # Reset _pounce to false -- this will cause checkPounceStatus() into a rescan self._pounce = False self._checkPounceStatus() return True def setLogTitle(self, title): if self.purrer: if title != self.purrer.logtitle: self.purrer.setLogTitle(title) self._updateViewer() self._updateNames() def _updateNames(self): self.wnewbtn.setEnabled(True) self.wviewlog.setEnabled(True) self._about_dialog.setText(self.about_message + """ <P>Your current log resides in:<PRE> <tt>%s</tt></PRE>To see your log in all its HTML-rendered glory, point your browser to <tt>index.html</tt> therein, or use the handy "View" button provided by PURR.</P> <P>Your current working directories are:</P> <P>%s</P> """ % (self.purrer.logdir, "".join(["<PRE> <tt>%s</tt></PRE>" % name for name, state in self.purrer.watchedDirectories()]) )) title = self.purrer.logtitle or "Unnamed log" self.title_editor.setText(title) self.viewer_dialog.setWindowTitle(title) def _showViewerDialog(self): self._updateViewer(True) self.viewer_dialog.show() @staticmethod def fileModTime(path): try: return os.path.getmtime(path) except: return None def _updateViewer(self, force=False): """Updates the viewer dialog. If dialog is not visible and force=False, does nothing. Otherwise, checks the mtime of the current purrer index.html file against self._viewer_timestamp. If it is newer, reloads it. """ if not force and not self.viewer_dialog.isVisible(): return # default text if nothing is found path = self.purrer.indexfile mtime = self.fileModTime(path) # return if file is older than our content if mtime and mtime <= (self._viewer_timestamp or 0): return busy = BusyIndicator() self.viewer_dialog.setDocument(path, empty= "<P>Nothing in the log yet. Try adding some log entries.</P>") self.viewer_dialog.reload() self.viewer_dialog.setLabel("""<P>Below is your full HTML-rendered log. Note that this is only a bare-bones viewer, so only a limited set of links will work. For a fully-functional view, use a proper HTML browser to look at the index file residing here:<BR> <tt>%s</tt></P> """ % self.purrer.indexfile) self._viewer_timestamp = mtime def _setEntries(self, entries): self.etw.clear() item = None self._index_paths = {} self._index_paths[os.path.abspath(self.purrer.indexfile)] = -1 for i, entry in enumerate(entries): item = self._addEntryItem(entry, i, item) self._index_paths[os.path.abspath(entry.index_file)] = i self.etw.resizeColumnToContents(0) def _titleChanged(self): self.setLogTitle(str(self.title_editor.text())) def _checkPounceStatus(self): ## pounce = bool([ entry for entry in self._dir_entries.itervalues() if entry.watching ]) pounce = bool([path for path, state in self.purrer.watchedDirectories() if state >= Purr.WATCHED]) # rescan, if going from not-pounce to pounce if pounce and not self._pounce: self._rescan() self._pounce = pounce # start timer -- we need it running to check the purr pipe, anyway self._timer.start(2000) def _forceRescan(self): if not self.purrer: self.attachDirectory('.') self._rescan(force=True) def _rescan(self, force=False): if not self.purrer: return # if pounce is on, tell the Purrer to rescan directories if self._pounce or force: dps = self.purrer.rescan() if dps: filenames = [dp.filename for dp in dps] dprint(2, "new data products:", filenames) self.message("Pounced on " + ", ".join(filenames)) if self.new_entry_dialog.addDataProducts(dps): dprint(2, "showing dialog") self.new_entry_dialog.show() # else read stuff from pipe for pipe in self.purrpipes.values(): do_show = False for command, show, content in pipe.read(): if command == "title": self.new_entry_dialog.suggestTitle(content) elif command == "comment": self.new_entry_dialog.addComment(content) elif command == "pounce": self.new_entry_dialog.addDataProducts(self.purrer.makeDataProducts( [(content, not show)], unbanish=True)) else: print("Unknown command received from Purr pipe: ", command) continue do_show = do_show or show if do_show: self.new_entry_dialog.show() def _addDPFiles(self, *files): """callback to add DPs corresponding to files.""" # quiet flag is always true self.new_entry_dialog.addDataProducts(self.purrer.makeDataProducts( [(file, True) for file in files], unbanish=True, unignore=True)) def _addDPFilesToOldEntry(self, *files): """callback to add DPs corresponding to files.""" # quiet flag is always true self.view_entry_dialog.addDataProducts(self.purrer.makeDataProducts( [(file, True) for file in files], unbanish=True, unignore=True)) def _entrySelectionChanged(self): selected = [item for item in self.etw.iterator(self.etw.Iterator.Selected) if item._ientry is not None] self.weditbtn.setEnabled(len(selected) == 1) self.wdelbtn.setEnabled(bool(selected)) def _viewEntryItem(self, item=None, *dum): """Pops up the viewer dialog for the entry associated with the given item. If 'item' is None, looks for a selected item in the listview. The dum arguments are for connecting this to QTreeWidget signals such as doubleClicked(). """ # if item not set, look for selected items in listview. Only 1 must be selected. select = True if item is None: selected = [item for item in self.etw.iterator(self.etw.Iterator.Selected) if item._ientry is not None] if len(selected) != 1: return item = selected[0] select = False; # already selected else: # make sure item is open -- the click will cause it to close self.etw.expandItem(item) # show dialog ientry = getattr(item, '_ientry', None) if ientry is not None: self._viewEntryNumber(ientry, select=select) def _viewEntryNumber(self, ientry, select=True): """views entry #ientry. Also selects entry in listview if select=True""" # pass entry to viewer dialog self._viewing_ientry = ientry entry = self.purrer.entries[ientry] busy = BusyIndicator() self.view_entry_dialog.viewEntry(entry, prev=ientry > 0 and self.purrer.entries[ientry - 1], next=ientry < len(self.purrer.entries) - 1 and self.purrer.entries[ientry + 1]) self.view_entry_dialog.show() # select entry in listview if select: self.etw.clearSelection() self.etw.setItemSelected(self.etw.topLevelItem(ientry), True) def _viewPrevEntry(self): if self._viewing_ientry is not None and self._viewing_ientry > 0: self._viewEntryNumber(self._viewing_ientry - 1) def _viewNextEntry(self): if self._viewing_ientry is not None and self._viewing_ientry < len(self.purrer.entries) - 1: self._viewEntryNumber(self._viewing_ientry + 1) def _viewPath(self, path): num = self._index_paths.get(os.path.abspath(path), None) if num is None: return elif num == -1: self.view_entry_dialog.hide() self._showViewerDialog() else: self._viewEntryNumber(num) def _showItemContextMenu(self, item, point, col): """Callback for contextMenuRequested() signal. Pops up item menu, if defined""" menu = getattr(item, '_menu', None) if menu: settitle = getattr(item, '_set_menu_title', None) if settitle: settitle() # self._current_item tells callbacks what item the menu was referring to point = self.etw.mapToGlobal(point) self._current_item = item self.etw.clearSelection() self.etw.setItemSelected(item, True) menu.exec_(point) else: self._current_item = None def _copyItemToClipboard(self): """Callback for item menu.""" if self._current_item is None: return dp = getattr(self._current_item, '_dp', None) if dp and dp.archived: path = dp.fullpath.replace(" ", "\\ ") QApplication.clipboard().setText(path, QClipboard.Clipboard) QApplication.clipboard().setText(path, QClipboard.Selection) def _restoreItemFromArchive(self): """Callback for item menu.""" if self._current_item is None: return dp = getattr(self._current_item, '_dp', None) if dp and dp.archived: dp.restore_from_archive(parent=self) def _deleteSelectedEntries(self): remaining_entries = [] del_entries = list(self.etw.iterator(self.etw.Iterator.Selected)) remaining_entries = list(self.etw.iterator(self.etw.Iterator.Unselected)) if not del_entries: return hide_viewer = bool([item for item in del_entries if self._viewing_ientry == item._ientry]) del_entries = [self.purrer.entries[self.etw.indexOfTopLevelItem(item)] for item in del_entries] remaining_entries = [self.purrer.entries[self.etw.indexOfTopLevelItem(item)] for item in remaining_entries] # ask for confirmation if len(del_entries) == 1: msg = """<P><NOBR>Permanently delete the log entry</NOBR> "%s"?</P>""" % del_entries[0].title if del_entries[0].dps: msg += """<P>%d data product(s) saved with this entry will be deleted as well.</P>""" % len(del_entries[0].dps) else: msg = """<P>Permanently delete the %d selected log entries?</P>""" % len(del_entries) ndp = 0 for entry in del_entries: ndp += len([dp for dp in entry.dps if not dp.ignored]) if ndp: msg += """<P>%d data product(s) saved with these entries will be deleted as well.</P>""" % ndp if QMessageBox.warning(self, "Deleting log entries", msg, QMessageBox.Yes, QMessageBox.No) != QMessageBox.Yes: return if hide_viewer: self.view_entry_dialog.hide() # reset entries in purrer and in our log window self._setEntries(remaining_entries) self.purrer.deleteLogEntries(del_entries) # self.purrer.setLogEntries(remaining_entries) # log will have changed, so update the viewer self._updateViewer() # delete entry files for entry in del_entries: entry.remove_directory() def _addEntryItem(self, entry, number, after): item = entry.tw_item = QTreeWidgetItem(self.etw, after) timelabel = self._make_time_label(entry.timestamp) item.setText(0, timelabel) item.setText(1, " " + (entry.title or "")) item.setToolTip(1, entry.title) if entry.comment: item.setText(2, " " + entry.comment.split('\n')[0]) item.setToolTip(2, "<P>" + entry.comment.replace("<", "<").replace(">", ">"). \ replace("\n\n", "</P><P>").replace("\n", "</P><P>") + "</P>") item._ientry = number item._dp = None item._menu = self._entry_menu item._set_menu_title = lambda: self._entry_menu_title.setText('"%s"' % entry.title) # now make subitems for DPs subitem = None for dp in entry.dps: if not dp.ignored: subitem = self._addDPSubItem(dp, item, subitem) self.etw.collapseItem(item) self.etw.header().headerDataChanged(Qt.Horizontal, 0, 2) return item def _addDPSubItem(self, dp, parent, after): item = QTreeWidgetItem(parent, after) item.setText(1, dp.filename) item.setToolTip(1, dp.filename) item.setText(2, dp.comment or "") item.setToolTip(2, dp.comment or "") item._ientry = None item._dp = dp item._menu = self._archived_dp_menu item._set_menu_title = lambda: self._archived_dp_menu_title.setText(os.path.basename(dp.filename)) return item def _make_time_label(self, timestamp): return time.strftime("%b %d %H:%M", time.localtime(timestamp)) def _newLogEntry(self, entry): """This is called when a new log entry is created""" # add entry to purrer self.purrer.addLogEntry(entry) # add entry to listview if it is not an ignored entry # (ignored entries only carry information about DPs to be ignored) if not entry.ignore: if self.etw.topLevelItemCount(): lastitem = self.etw.topLevelItem(self.etw.topLevelItemCount() - 1) else: lastitem = None self._addEntryItem(entry, len(self.purrer.entries) - 1, lastitem) self._index_paths[os.path.abspath(entry.index_file)] = len(self.purrer.entries) - 1 # log will have changed, so update the viewer if not entry.ignore: self._updateViewer() self.show() def _entryChanged(self, entry): """This is called when a log entry is changed""" # resave the log self.purrer.save() # redo entry item if entry.tw_item: number = entry.tw_item._ientry entry.tw_item = None self.etw.takeTopLevelItem(number) if number: after = self.etw.topLevelItem(number - 1) else: after = None self._addEntryItem(entry, number, after) # log will have changed, so update the viewer self._updateViewer() def _regenerateLog(self): if QMessageBox.question(self.viewer_dialog, "Regenerate log", """<P><NOBR>Do you really want to regenerate the entire</NOBR> log? This can be a time-consuming operation.</P>""", QMessageBox.Yes, QMessageBox.No) != QMessageBox.Yes: return self.purrer.save(refresh=True) self._updateViewer()