def subscribe(self, icepap_addr, signal_name): """ Creates a new subscription for signal values. icepap_addr - IcePAP driver number. signal_name - Signal name. Return - A positive integer id used when unsubscribing. """ for ch in self.channels_subscribed.values(): if ch.equals(icepap_addr, signal_name): msg = 'Channel already exists.\nAddr: ' \ '{}\nSignal: {}'.format(icepap_addr, signal_name) raise Exception(msg) channel = Channel(icepap_addr, signal_name) sn = QString(signal_name) cond_1 = sn.endsWith('Tgtenc') cond_2 = sn.endsWith('Shftenc') cond_3 = sn == 'DifAxMeasure' if cond_1 or cond_2 or cond_3: try: cfg = self.icepap_system[icepap_addr].get_cfg() except RuntimeError as e: msg = 'Failed to retrieve configuration parameters ' \ 'for driver {}\n{}.'.format(icepap_addr, e) raise Exception(msg) if (cond_1 and cfg['TGTENC'].upper() == 'NONE') or \ (cond_2 and cfg['SHFTENC'].upper() == 'NONE'): msg = 'Signal {} is not mapped/valid.'.format(sn) raise Exception(msg) if cond_3: channel.set_measure_resolution(cfg) self.channel_id += 1 self.channels_subscribed[self.channel_id] = channel return self.channel_id
def saveProjectAs(self): ''' Save the project configuration under a different name ''' try: # get the location for the new config file on disk start_dir = paths.OPUS_PROJECT_CONFIGS_PATH configDialog = QFileDialog() filter_str = QString("*.xml") fd = configDialog.getSaveFileName(self,QString("Save As..."), QString(start_dir), filter_str) # Check for cancel if not fd: return filename = QString(fd) # append xml extension if no extension was given if not filename.endsWith('.xml') and len(filename.split('.')) == 1: filename = filename + '.xml' if not self.saveProject(filename): return # hack: open the project right after save to properly change the # 'active project' related parameters self.openProject(filename) except: errorMessage = formatExceptionInfo(custom_message = \ 'Unexpected error saving config') QMessageBox.warning(self, 'Warning', errorMessage)
def saveProjectAs(self): ''' Save the project configuration under a different name ''' try: # get the location for the new config file on disk start_dir = paths.OPUS_PROJECT_CONFIGS_PATH configDialog = QFileDialog() filter_str = QString("*.xml") fd = configDialog.getSaveFileName(self, QString("Save As..."), QString(start_dir), filter_str) # Check for cancel if not fd: return filename = QString(fd) # append xml extension if no extension was given if not filename.endsWith('.xml') and len(filename.split('.')) == 1: filename = filename + '.xml' if not self.saveProject(filename): return # hack: open the project right after save to properly change the # 'active project' related parameters self.openProject(filename) except: errorMessage = formatExceptionInfo(custom_message = \ 'Unexpected error saving config') QMessageBox.warning(self, 'Warning', errorMessage)
def send_to_process(self, qstr): if not isinstance(qstr, QString): qstr = QString(qstr) if not qstr.endsWith('\n'): qstr.append('\n') self.process.write(qstr.toLocal8Bit()) self.process.waitForBytesWritten(-1)
def send_to_process(self, qstr): if not isinstance(qstr, QString): qstr = QString(qstr) if not qstr.endsWith('\n'): qstr.append('\n') self.process.write(qstr.toLocal8Bit()) self.process.waitForBytesWritten(-1) # Eventually write prompt faster (when hitting Enter continuously) # -- necessary/working on Windows only: if os.name == 'nt': self.write_error()
def send_to_process(self, qstr): if not isinstance(qstr, QString): qstr = QString(qstr) if qstr[:-1] in ["clear", "cls", "CLS"]: self.shell.clear() self.send_to_process(QString(os.linesep)) return if not qstr.endsWith('\n'): qstr.append('\n') if os.name == 'nt': self.process.write(unicode(qstr).encode('cp850')) else: self.process.write(qstr.toLocal8Bit()) self.process.waitForBytesWritten(-1)
def updateTable(self, id): filePath = QFileDialog.getOpenFileName(self, "请选择库", self.alloneEnv, "Library(*.lib)") if filePath.isEmpty(): return fileinfo = QFileInfo(filePath) libPath = fileinfo.absoluteDir().absolutePath() libName = fileinfo.baseName() # 支持选择文件后与系统ALLONEDIR比较一下变成相对路径 # 并且能够手动输入相对路径或包含$(ALLONEDIR)的相对路径 env = QString(os.getenv('ALLONEDIR', '../..').replace('\\', '/')) if env.endsWith('/'): env.remove(env.lastIndexOf('/'), 1) if libPath.contains(env): libPath.replace(env, QString('$$ALLONEDIR')) self.tw_interface.setItem(id, 1, QTableWidgetItem(libPath)) self.tw_interface.setItem(id, 0, QTableWidgetItem(libName))
def refresh(self, clearCache=False): # this could be first refresh for this book file, so set the # base URL for its images. sep = QChar(u'/') qsp = QString(IMC.bookDirPath) if not qsp.endsWith(sep): qsp.append(sep) self.baseURL = QUrl.fromLocalFile(qsp) # this might be the second or nth refresh of the book, note the # scroll position so we can restore it in loadEnds below. This # means that when you make a little edit at the end of a book, and # refresh the preview, you won't have to scroll down to the end # for the 500th time to see your changes. self.scrollPosition = self.webPage.mainFrame().scrollPosition() if clearCache: self.settings.clearMemoryCaches() # We are reloading our base page, so clear any history of prior links self.history.clear() self.preview.setHtml(IMC.editWidget.toPlainText(), self.baseURL)
class MainWindowStart(QMainWindow, MainWindow_Pro.Ui_MainWindow): def __init__(self, parent=None): super(MainWindowStart, self).__init__(parent) # Mappers for connecting buttons and labels self.myMapper = QSignalMapper(self) self.myMapper_StyleSheet = QSignalMapper(self) # Load UI self.setupUi(self) self.regex_edits = QRegExp(r"(^[0]+$|^$)") self._filter = Filter() self.filename = QString() self.edit1_delayh.installEventFilter(self._filter) self.sizeLabel = QLabel() self.sizeLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar1.addPermanentWidget(self.sizeLabel) self.statusBar1.setSizeGripEnabled(False) self.create_connections() self.assign_shortcuts() self.create_tool_bar() self.update_devices_list() # self.button_stop.clicked.connect(self.stop_all) # List of valve pushbuttons self.valve_list = [ self.valve1, self.valve2, self.valve3, self.valve4, self.valve5, self.valve6, self.valve7, self.valve8 ] # GroupBoxes for grouping labels and buttons on each row, used for applying StyleSheets self.group_boxes = [ self.groupbox1, self.groupbox2, self.groupbox3, self.groupbox4, self.groupbox5, self.groupbox6, self.groupbox7, self.groupbox8 ] # List of lineEdits self.lineEdits_list = [ (self.edit1_delayh, self.edit1_delaym, self.edit1_delays, self.edit1_onh, self.edit1_onm, self.edit1_ons, self.edit1_offh, self.edit1_offm, self.edit1_offs, self.edit1_totalh, self.edit1_totalm, self.edit1_totals), (self.edit2_delayh, self.edit2_delaym, self.edit2_delays, self.edit2_onh, self.edit2_onm, self.edit2_ons, self.edit2_offh, self.edit2_offm, self.edit2_offs, self.edit2_totalh, self.edit2_totalm, self.edit2_totals), (self.edit3_delayh, self.edit3_delaym, self.edit3_delays, self.edit3_onh, self.edit3_onm, self.edit3_ons, self.edit3_offh, self.edit3_offm, self.edit3_offs, self.edit3_totalh, self.edit3_totalm, self.edit3_totals), (self.edit4_delayh, self.edit4_delaym, self.edit4_delays, self.edit4_onh, self.edit4_onm, self.edit4_ons, self.edit4_offh, self.edit4_offm, self.edit4_offs, self.edit4_totalh, self.edit4_totalm, self.edit4_totals), (self.edit5_delayh, self.edit5_delaym, self.edit5_delays, self.edit5_onh, self.edit5_onm, self.edit5_ons, self.edit5_offh, self.edit5_offm, self.edit5_offs, self.edit5_totalh, self.edit5_totalm, self.edit5_totals), (self.edit6_delayh, self.edit6_delaym, self.edit6_delays, self.edit6_onh, self.edit6_onm, self.edit6_ons, self.edit6_offh, self.edit6_offm, self.edit6_offs, self.edit6_totalh, self.edit6_totalm, self.edit6_totals), (self.edit7_delayh, self.edit7_delaym, self.edit7_delays, self.edit7_onh, self.edit7_onm, self.edit7_ons, self.edit7_offh, self.edit7_offm, self.edit7_offs, self.edit7_totalh, self.edit7_totalm, self.edit7_totals), (self.edit8_delayh, self.edit8_delaym, self.edit8_delays, self.edit8_onh, self.edit8_onm, self.edit8_ons, self.edit8_offh, self.edit8_offm, self.edit8_offs, self.edit8_totalh, self.edit8_totalm, self.edit8_totals) ] for index, editLabels in enumerate(self.lineEdits_list, 1): for index2, lineedits in enumerate(editLabels, 0): # Apply mapper (GUIObject, objectIndex) self.myMapper_StyleSheet.setMapping( self.lineEdits_list[index - 1][index2], index - 1) # Connect mapper to signal (self.lineEdits_list[index - 1][index2]).textChanged.connect( self.myMapper_StyleSheet.map) # Set event Filter, for detecting when Focus changes self.lineEdits_list[index - 1][index2].installEventFilter( self._filter) # Set Mappers for buttons (1..8) self.myMapper.setMapping(self.valve_list[index - 1], index) # Connect mapper to signal for detecting clicks on buttons (self.valve_list[index - 1]).clicked.connect(self.myMapper.map) # Connect to signal for enabling labelEdits self.myMapper.mapped['int'].connect(self.enable_fields) # Connect to signal for changing color of groupbox used for visual indication self.myMapper_StyleSheet.mapped['int'].connect(self.valve_color_status) # Create Keyboard Shortcuts def assign_shortcuts(self): self.actionArchivo_Nuevo.setShortcut(QKeySequence.New) self.action_Abrir.setShortcut(QKeySequence.Open) self.action_Guardar.setShortcut(QKeySequence.Save) self.actionGuardar_Como.setShortcut(QKeySequence.SaveAs) self.action_Limpiar.setShortcut('Ctrl+L') self.actionVAL_508_Ayuda.setShortcut(QKeySequence.HelpContents) self.action_Salir.setShortcut(QKeySequence.Close) # self.actionPreferencias.setShortcut(QKeySequence.Preferences) self.action_Detener_USB.setShortcut('Ctrl+Shift+C') self.action_Ejecutar.setShortcut('Ctrl+Shift+X') self.action_Para_Valvulas.setShortcut('Ctrl+Shift+P') # Create connections to signals def create_connections(self): self.actionArchivo_Nuevo.triggered.connect(self.new_file) self.action_Abrir.triggered.connect(self.open_file) self.action_Guardar.triggered.connect(self.save_file) self.actionGuardar_Como.triggered.connect(self.save_file_as) self.action_Limpiar.triggered.connect(self.clean_fields) self.action_Salir.triggered.connect(self.close) self.actionVAL_508_Ayuda.triggered.connect(self.show_help) self.actionAcerca_de_VAL_508.triggered.connect(self.show_about) self.action_Detener_USB.triggered.connect(self.stop_usb) self.action_Ejecutar.triggered.connect(self.execute) self.action_Para_Valvulas.triggered.connect(self.stop_all) # Creation of About Dialog def show_about(self): about = aboutdialog.AboutDialog(self) about.show() # Creation of Help Form def show_help(self): form = helpform.HelpForm('Help.html', self) form.show() def new_file(self): self.filename = QString() self.clean_fields() # Close connection to arduino before closing Program def closeEvent(self, QCloseEvent): try: self.thread_connection.serial_connection.close() logging.debug("Thread running and killed at closing program") except AttributeError: logging.debug("Thread was not running when closing program OK") def clean_fields(self): for index, editLabels in enumerate(self.lineEdits_list, 1): for index2, lineedits in enumerate(editLabels, 0): self.lineEdits_list[index - 1][index2].setText('0') def save_file_as(self): filename_copy = self.filename logging.info("Current filename: %s" % self.filename) my_home = os.path.expanduser('~') self.filename = QFileDialog.getSaveFileName( self, self.tr('Guardar como'), os.path.join(my_home, "archivo.txt"), "", "", QFileDialog.DontUseNativeDialog) logging.info("Filename to save: %s" % self.filename) if not self.filename.isNull(): if self.filename.endsWith(QString('.txt')): self.write_data_to_file('w') else: self.filename.append(QString('.txt')) messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr('Advertencia')) messageBox.setText( self.tr(u"El archivo ya existe, ¿Reemplazar?")) messageBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) messageBox.setIconPixmap(QPixmap(':/broken_file.png')) if messageBox.exec_() == QMessageBox.Yes: self.write_data_to_file('w') else: try: while True: self.filename = QFileDialog.getSaveFileName( self, self.tr('Guardar como'), os.path.join(my_home, "archivo.txt"), "", "", QFileDialog.DontUseNativeDialog) if self.filename.isNull(): raise Saved_Canceled() else: messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}' ) messageBox.setWindowTitle( self.tr('Advertencia')) messageBox.setText( self.tr( u"El archivo ya existe, ¿Reemplazar?")) messageBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) messageBox.setIconPixmap( QPixmap(':/broken_file.png')) if messageBox.exec_() == QMessageBox.Yes: self.write_data_to_file('w') raise Saved_Accepted() except Saved_Canceled: self.filename = filename_copy except Saved_Accepted: pass logging.info("Current filename after operation: %s" % self.filename) def save_file(self): if self.filename.isNull(): self.save_file_as() else: self.write_data_to_file('w') # Colect data for arduino def execute(self): string_data = '' list_strings = [] if str(self.arduino_combobox.currentText()): self.statusBar1.showMessage(self.tr('Conectando...')) # Gather all the contents of each row of valves and create a list of lists with them for elem_edit in self.lineEdits_list: # delay string_data = string_data + str( ((int(elem_edit[0].text()) * 3600) + (int(elem_edit[1].text()) * 60) + (int(elem_edit[2].text()))) * 1000) + ';' # ON string_data = string_data + str( ((int(elem_edit[3].text()) * 3600) + (int(elem_edit[4].text()) * 60) + (int(elem_edit[5].text()))) * 1000) + ';' # OFF string_data = string_data + str( ((int(elem_edit[6].text()) * 3600) + (int(elem_edit[7].text()) * 60) + (int(elem_edit[8].text()))) * 1000) + ';' # Total string_data = string_data + str( ((int(elem_edit[9].text()) * 3600) + (int(elem_edit[10].text()) * 60) + (int(elem_edit[11].text()))) * 1000) + ';' list_strings.append(string_data) string_data = '' # Start QThread for communicating with arduino self.thread_connection = Arduino_Communication( str(self.arduino_combobox.currentText()), list_strings) self.thread_connection.start() self.action_Ejecutar.setEnabled(False) self.action_Para_Valvulas.setEnabled(False) # Connect to current QThread instance in order to know the status of it's termination # This line used only when stopping current task self.thread_connection.finished.connect(self.finished_thread) self.thread_connection.connection_exit_status.connect( self.finished_thread) else: messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr('Advertencia')) messageBox.setText(self.tr("Arduino no seleccionado")) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.setIconPixmap(QPixmap(':/usb_error.png')) messageBox.exec_() # Inform QThread to stop sending data to arduino def stop_usb(self): if str(self.arduino_combobox.currentText()): try: self.statusBar1.showMessage(self.tr(u'Conexión detenida')) if self.thread_connection.isRunning(): mutex.lock() self.thread_connection.kill_serial = True mutex.unlock() except AttributeError: logging.debug("Thread not running \'disconnected! \'") else: messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr('Advertencia')) messageBox.setText(self.tr("Arduino no seleccionado")) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.setIconPixmap(QPixmap(':/usb_error.png')) messageBox.exec_() def enable_fields(self, index): hours_reg = QRegExp(r"0*[0-9]{1,3}") sec_reg = QRegExp(r"(0*[0-9])|(0*[0-5][0-9])") for counter, line_edit in enumerate(self.lineEdits_list[index - 1]): line_edit.setEnabled(self.valve_list[index - 1].isChecked()) if counter % 3 == 0: line_edit.setValidator(QRegExpValidator(hours_reg, self)) else: line_edit.setValidator(QRegExpValidator(sec_reg, self)) def valve_color_status(self, index): logging.info("Checking color from valve button") for edit in self.lineEdits_list[index]: if edit.text().contains(self.regex_edits): self.group_boxes[index].setStyleSheet('''QGroupBox { border: 2px solid; border-color: rgba(255, 255, 255, 0);}''' ) else: self.group_boxes[index].setStyleSheet( '''QGroupBox {background-color: rgba(103, 255, 126, 150); border: 2px solid; border-color: rgba(255, 255, 255, 255);}''' ) break def create_tool_bar(self): self.label_arduino = QLabel(self.tr('Dispositivos: ')) self.toolBar.addWidget(self.label_arduino) self.arduino_combobox = QComboBox() self.arduino_combobox.setToolTip(self.tr('Seleccionar Arduino')) self.arduino_combobox.setFocusPolicy(Qt.NoFocus) # Update List of Arduino devices self.reload = QAction(QIcon(":/reload.png"), self.tr("&Refrescar"), self) self.reload.setShortcut(QKeySequence.Refresh) self.reload.setToolTip(self.tr('Refrescar Dispositivos')) self.reload.triggered.connect(self.update_devices_list) self.toolBar.addWidget(self.arduino_combobox) self.toolBar.addAction(self.reload) # Update current usb devices connected to PC def update_devices_list(self): device_list = serial.tools.list_ports.comports() current_arduino = self.arduino_combobox.currentText() self.arduino_combobox.clear() for device_index, device in enumerate(sorted(device_list)): self.arduino_combobox.addItem(device.device) if device.device == current_arduino: self.arduino_combobox.setCurrentIndex(device_index) # Stop current arduino task def stop_all(self): if str(self.arduino_combobox.currentText()): self.thread_connection = Arduino_Communication( str(self.arduino_combobox.currentText())) self.thread_connection.start() self.action_Ejecutar.setEnabled(False) self.action_Para_Valvulas.setEnabled(False) self.action_Detener_USB.setEnabled(False) self.thread_connection.finished.connect(self.finished_thread) self.thread_connection.connection_exit_status.connect( self.finished_thread) else: messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr('Advertencia')) messageBox.setText(self.tr("Arduino no seleccionado")) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.setIconPixmap(QPixmap(':/usb_error.png')) messageBox.exec_() def open_file(self): try: my_home = os.path.expanduser('~') file_name = QFileDialog.getOpenFileName( self, self.tr('Abrir archivo'), my_home, '*.txt', '*.txt', QFileDialog.DontUseNativeDialog) logging.warning("file_name type: %s" % type(file_name)) list_values = [] if not file_name.isNull(): with open(file_name) as fp: for line in fp: list_values.extend([line.replace('\n', '')]) logging.info("List Content: %s" % list_values) count = 0 for elems in self.lineEdits_list: for inner_elem in elems: if not unicode(list_values[count]).isdigit(): raise Uncompatible_Data() inner_elem.setText(list_values[count]) count = count + 1 self.filename = file_name except (IOError, OSError): messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr('Error')) messageBox.setText(self.tr('No se pudo abrir el archivo')) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.setIconPixmap(QPixmap(':/broken_file.png')) messageBox.exec_() except (IndexError, Uncompatible_Data): messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr('Error')) messageBox.setText(self.tr('Formato Incompatible')) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.setIconPixmap(QPixmap(':/broken_file.png')) messageBox.exec_() # Inform the user if we were able to send data successfully to arduino def finished_thread(self, error=None, message=''): if error == 'error': messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr('Error')) messageBox.setText(message) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.setIconPixmap(QPixmap(':/usb_error.png')) messageBox.exec_() return elif error == 'success': messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr(u'Éxito')) messageBox.setText(message) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.setIconPixmap(QPixmap(':/usb_success.png')) messageBox.exec_() return elif error == 'stopped': messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr(u'Éxito')) messageBox.setText(message) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.setIconPixmap(QPixmap(':/success_general.png')) messageBox.exec_() return self.action_Ejecutar.setEnabled(True) self.action_Para_Valvulas.setEnabled(True) self.action_Detener_USB.setEnabled(True) self.statusBar1.showMessage(self.tr('Finalizado')) # Save data to disk def write_data_to_file(self, open_mode): progressDialog = QProgressDialog() progressDialog.setModal(True) progressDialog.setLabelText(self.tr('Guardando...')) progressDialog.setMaximum(8) progressDialog.setCancelButton(None) progressDialog.show() try: # File is closed automatically even on error with open(unicode(self.filename), open_mode) as file_obj: for count, elem_edit in enumerate(self.lineEdits_list, 1): file_obj.write(''.join([str(elem_edit[0].text()), '\n'])) file_obj.write(''.join([str(elem_edit[1].text()), '\n'])) file_obj.write(''.join([str(elem_edit[2].text()), '\n'])) file_obj.write(''.join([str(elem_edit[3].text()), '\n'])) file_obj.write(''.join([str(elem_edit[4].text()), '\n'])) file_obj.write(''.join([str(elem_edit[5].text()), '\n'])) file_obj.write(''.join([str(elem_edit[6].text()), '\n'])) file_obj.write(''.join([str(elem_edit[7].text()), '\n'])) file_obj.write(''.join([str(elem_edit[8].text()), '\n'])) file_obj.write(''.join([str(elem_edit[9].text()), '\n'])) file_obj.write(''.join([str(elem_edit[10].text()), '\n'])) file_obj.write(''.join([str(elem_edit[11].text()), '\n'])) progressDialog.setValue(count) except (IOError, OSError): progressDialog.close() messageBox = QMessageBox(self) messageBox.setStyleSheet( 'QMessageBox QLabel {font: bold 14pt "Cantarell";}') messageBox.setWindowTitle(self.tr('Error')) messageBox.setText(self.tr('Error al guardar')) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.setIcon(QMessageBox.Critical) messageBox.exec_() else: self.statusBar1.showMessage(self.tr('Guardado'), 3000)
class MovieContainer(object): """A MovieContainer holds a set of Movie objects. The movies are held in a canonicalized order based on their title and year, so if either of these fields is changed the movies must be re-sorted. For this reason (and to maintain the dirty flag), all updates to movies should be made through this class's updateMovie() method. """ MAGIC_NUMBER = 0x3051E OLD_FILE_VERSION = 100 FILE_VERSION = 101 def __init__(self): self.__fname = QString() self.__movies = [] self.__movieFromId = {} self.__dirty = False def key(self, title, year): text = str(title).lower() if text.startswith("a "): text = text[2:] elif text.startswith("an "): text = text[3:] elif text.startswith("the "): text = text[4:] parts = text.split(" ", 1) if parts[0].isdigit(): text = "{0:08d} ".format(int(parts[0])) if len(parts) > 1: text += parts[1] return "{0}\t{1}".format(text.replace(" ", ""), year) def isDirty(self): return self.__dirty def setDirty(self, dirty=True): self.__dirty = dirty def clear(self, clearFilename=True): self.__movies = [] self.__movieFromId = {} if clearFilename: self.__fname = QString() self.__dirty = False def movieFromId(self, id): """Returns the movie with the given Python ID.""" return self.__movieFromId[id] def movieAtIndex(self, index): """Returns the index-th movie.""" return self.__movies[index][1] def add(self, movie): """Adds the given movie to the list if it isn't already present. Returns True if added; otherwise returns False.""" if id(movie) in self.__movieFromId: return False key = self.key(movie.title, movie.year) bisect.insort_left(self.__movies, [key, movie]) self.__movieFromId[id(movie)] = movie self.__dirty = True return True def delete(self, movie): """Deletes the given movie from the list and returns True; returns False if the movie isn't in the list.""" if id(movie) not in self.__movieFromId: return False key = self.key(movie.title, movie.year) i = bisect.bisect_left(self.__movies, [key, movie]) del self.__movies[i] del self.__movieFromId[id(movie)] self.__dirty = True return True def updateMovie(self, movie, title, year, minutes=None, location=None, notes=None): if minutes is not None: movie.minutes = minutes if location is not None: movie.location = location if notes is not None: movie.notes = notes if title != movie.title or year != movie.year: key = self.key(movie.title, movie.year) i = bisect.bisect_left(self.__movies, [key, movie]) self.__movies[i][0] = self.key(title, year) movie.title = title movie.year = year self.__movies.sort() self.__dirty = True def __iter__(self): for pair in iter(self.__movies): yield pair[1] def __len__(self): return len(self.__movies) def setFilename(self, fname): self.__fname = fname def filename(self): return self.__fname @staticmethod def formats(): return "*.mqb" def save(self, fname=QString()): if not fname.isEmpty(): self.__fname = fname if self.__fname.endsWith(".mqb"): return self.saveQDataStream() return False, "Failed to save: invalid file extension" def load(self, fname=QString()): if not fname.isEmpty(): self.__fname = fname if self.__fname.endsWith(".mqb"): return self.loadQDataStream() return False, "Failed to load: invalid file extension" def saveQDataStream(self): error = None fh = None try: fh = QFile(self.__fname) if not fh.open(QIODevice.WriteOnly): raise IOError(str(fh.errorString())) stream = QDataStream(fh) stream.writeInt32(MovieContainer.MAGIC_NUMBER) stream.writeInt32(MovieContainer.FILE_VERSION) stream.setVersion(QDataStream.Qt_4_2) for key, movie in self.__movies: stream << movie.title stream.writeInt16(movie.year) stream.writeInt16(movie.minutes) stream << movie.acquired << movie.location \ << movie.notes except EnvironmentError as e: error = "Failed to save: {0}".format(e) finally: if fh is not None: fh.close() if error is not None: return False, error self.__dirty = False return True, "Saved {0} movie records to {1}".format( len(self.__movies), QFileInfo(self.__fname).fileName()) def loadQDataStream(self): error = None fh = None try: fh = QFile(self.__fname) if not fh.open(QIODevice.ReadOnly): raise IOError(str(fh.errorString())) stream = QDataStream(fh) magic = stream.readInt32() if magic != MovieContainer.MAGIC_NUMBER: raise IOError("unrecognized file type") version = stream.readInt32() if version < MovieContainer.OLD_FILE_VERSION: raise IOError("old and unreadable file format") elif version > MovieContainer.FILE_VERSION: raise IOError("new and unreadable file format") old = False if version == MovieContainer.OLD_FILE_VERSION: old = True stream.setVersion(QDataStream.Qt_4_2) self.clear(False) while not stream.atEnd(): title = QString() acquired = QDate() location = QString() notes = QString() stream >> title year = stream.readInt16() minutes = stream.readInt16() if old: stream >> acquired >> notes else: stream >> acquired >> location >> notes self.add(Movie(title, year, minutes, acquired, location, notes)) except EnvironmentError as e: error = "Failed to load: {0}".format(e) finally: if fh is not None: fh.close() if error is not None: return False, error self.__dirty = False return True, "Loaded {0} movie records from {1}".format( len(self.__movies), QFileInfo(self.__fname).fileName()) def exportXml(self, fname): error = None fh = None try: fh = QFile(fname) if not fh.open(QIODevice.WriteOnly): raise IOError(str(fh.errorString())) stream = QTextStream(fh) stream.setCodec(CODEC) stream << ("<?xml version='1.0' encoding='{0}'?>\n" "<!DOCTYPE MOVIES>\n" "<MOVIES VERSION='1.0'>\n".format(CODEC)) for key, movie in self.__movies: stream << ("<MOVIE YEAR='{0}' MINUTES='{1}' " "ACQUIRED='{2}'>\n".format(movie.year, movie.minutes, movie.acquired.toString(Qt.ISODate))) \ << "<TITLE>" << Qt.escape(movie.title) \ << "</TITLE>\n" << "<LOCATION>" if not movie.location.isEmpty(): stream << "\n" << Qt.escape(movie.location) stream << "\n</LOCATION>\n" << "<NOTES>" if not movie.notes.isEmpty(): stream << "\n" << Qt.escape(encodedNewlines(movie.notes)) stream << "\n</NOTES>\n</MOVIE>\n" stream << "</MOVIES>\n" except EnvironmentError as e: error = "Failed to export: {0}".format(e) finally: if fh is not None: fh.close() if error is not None: return False, error self.__dirty = False return True, "Exported {0} movie records to {1}".format( len(self.__movies), QFileInfo(fname).fileName()) def importDOM(self, fname): dom = QDomDocument() error = None fh = None try: fh = QFile(fname) if not fh.open(QIODevice.ReadOnly): raise IOError(str(fh.errorString())) if not dom.setContent(fh): raise ValueError("could not parse XML") except (IOError, OSError, ValueError) as e: error = "Failed to import: {0}".format(e) finally: if fh is not None: fh.close() if error is not None: return False, error try: self.populateFromDOM(dom) except ValueError as e: return False, "Failed to import: {0}".format(e) self.__fname = QString() self.__dirty = True return True, "Imported {0} movie records from {1}".format( len(self.__movies), QFileInfo(fname).fileName()) def populateFromDOM(self, dom): root = dom.documentElement() if root.tagName() != "MOVIES": raise ValueError("not a Movies XML file") self.clear(False) node = root.firstChild() while not node.isNull(): if node.toElement().tagName() == "MOVIE": self.readMovieNode(node.toElement()) node = node.nextSibling() def readMovieNode(self, element): def getText(node): child = node.firstChild() text = QString() while not child.isNull(): if child.nodeType() == QDomNode.TextNode: text += child.toText().data() child = child.nextSibling() return text.trimmed() year = intFromQStr(element.attribute("YEAR")) minutes = intFromQStr(element.attribute("MINUTES")) ymd = element.attribute("ACQUIRED").split("-") if ymd.count() != 3: raise ValueError("invalid acquired date {0}".format( str(element.attribute("ACQUIRED")))) acquired = QDate(intFromQStr(ymd[0]), intFromQStr(ymd[1]), intFromQStr(ymd[2])) title = notes = None location = QString() node = element.firstChild() while title is None or notes is None: if node.isNull(): raise ValueError("missing title or notes") if node.toElement().tagName() == "TITLE": title = getText(node) elif node.toElement().tagName() == "LOCATION": location = getText(node) elif node.toElement().tagName() == "NOTES": notes = getText(node) node = node.nextSibling() self.add( Movie(title, year, minutes, acquired, location, decodedNewlines(notes))) def importSAX(self, fname): error = None fh = None try: handler = SaxMovieHandler(self) parser = QXmlSimpleReader() parser.setContentHandler(handler) parser.setErrorHandler(handler) fh = QFile(fname) input = QXmlInputSource(fh) self.clear(False) if not parser.parse(input): raise ValueError(handler.error) except (IOError, OSError, ValueError) as e: error = "Failed to import: {0}".format(e) finally: if fh is not None: fh.close() if error is not None: return False, error self.__fname = QString() self.__dirty = True return True, "Imported {0} movie records from {1}".format( len(self.__movies), QFileInfo(fname).fileName())