class FileSystemWatcher(QObject): fileChanged = pyqtSignal(str) directoryChanged = pyqtSignal(str) def __init__(self, parent = None): super().__init__(parent) self.mWatchCount = QMap() self.mWatcher = QFileSystemWatcher(self) self.mWatcher.fileChanged.connect(self.onFileChanged) self.mWatcher.directoryChanged.connect(self.onDirectoryChanged) def addPath(self, path): # Just silently ignore the request when the file doesn't exist if (not QFile.exists(path)): return entry = self.mWatchCount.find(path) if not entry: self.mWatcher.addPath(path) self.mWatchCount.insert(path, 1) else: # Path is already being watched, increment watch count self.mWatchCount[path] += 1 def removePath(self, path): entry = self.mWatchCount.find(path) if (entry == self.mWatchCount.end()): if (QFile.exists(path)): qWarning("FileSystemWatcher: Path was never added:\n"+path) return # Decrement watch count entry -= 1 self.mWatchCount[path] = entry if (entry == 0): self.mWatchCount.erase(path) self.mWatcher.removePath(path) def onFileChanged(self, path): # If the file was replaced, the watcher is automatically removed and needs # to be re-added to keep watching it for changes. This happens commonly # with applications that do atomic saving. if (not self.mWatcher.files().__contains__(path)): if (QFile.exists(path)): self.mWatcher.addPath(path) self.fileChanged.emit(path) def onDirectoryChanged(self, path): self.directoryChanged.emit(path)
class WatchFileWidget(QWidget): display_signal = pyqtSignal(str) def __init__(self, fnam='.log'): super(WatchFileWidget, self).__init__() self.fnam = fnam self.last_read = None self.min_time = 1e-1 def start(self): # if the file does not exist # create it then watch it if not os.path.exists(self.fnam): with open(self.fnam, 'w') as f: pass # watch file self.w = QFileSystemWatcher([self.fnam]) self.w.fileChanged.connect(self.fileChanged) def fileChanged(self): # if the file was read less than self.min_time ago, then skip if (self.last_read is not None) and (time.time() - self.last_read) < self.min_time: return # read the file and print output with open(self.fnam) as f: lines = f.readlines() last_line = lines[-1] print('last_line:', last_line) self.last_read = time.time() self.display_signal.emit(last_line.split('display:')[1].strip()) def stop(self): self.w.removePath(self.fnam)
class PugdebugDocuments(QObject): watcher = None open_documents = {} document_changed = pyqtSignal(object) document_removed = pyqtSignal(object) def __init__(self): super(PugdebugDocuments, self).__init__() self.watcher = QFileSystemWatcher() self.watcher.fileChanged.connect(self.handle_file_changed) def open_document(self, path): path_key = self.get_path_key(path) document = PugdebugDocument(path) self.open_documents[path_key] = document self.watcher.addPath(path) return document def close_document(self, path): path_key = self.get_path_key(path) self.open_documents.pop(path_key, None) self.watcher.removePath(path) def refresh_document(self, path): """Refresh a document Gets called when the file system watcher notices a change to an open document. """ path_key = self.get_path_key(path) document = self.open_documents[path_key] document.read_file(path) self.document_changed.emit(document) def is_document_open(self, path): path_key = self.get_path_key(path) return path_key in self.open_documents def handle_file_changed(self, path): """Handle when a watched file gets changed Crazy stuff ahead. If a file is modified, some editors (systems?) will first remove the file and then write it back to the disk. And for that split second, the watcher will drop the file from being watched. But then again, maybe that file really got deleted? Who knows?! Anyway, when a file gets modified, we sleep a short while to see if that file will "get back" and if so, add it back to the watcher. If not, we'll assume the file got deleted. """ if not self.__is_path_watched(path): fileinfo = QFileInfo(path) total_slept = 0 file_exists = fileinfo.exists() while not file_exists: sleep_for = 0.1 total_slept += sleep_for if total_slept > 1: break time.sleep(sleep_for) file_exists = fileinfo.exists() if file_exists: self.watcher.addPath(path) self.refresh_document(path) else: # file got deleted? path_key = self.get_path_key(path) document = self.open_documents[path_key] self.document_removed.emit(document) elif self.__is_path_watched(path): self.refresh_document(path) def get_path_key(self, path): path_key = hashlib.md5(path.encode('utf-8')) return path_key.hexdigest() def __is_path_watched(self, path): return path in self.watcher.files()
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self, ): super(MainWindow, self).__init__() self.setupUi(self) self.fig_dict = {} self.tabWidget.setCurrentIndex(0) self.plotAreaVerticalLayout = QtWidgets.QVBoxLayout() self.plotsFrame.setLayout(self.plotAreaVerticalLayout) #initialize Table self.headerV = self.tableWidget.verticalHeader() self.headerV.show() # add a widget for previewing plots, they can then be added to the actual plot self.plotPreview = PlotWidget() self.plotAreaVerticalLayout.addWidget(self.plotPreview) # Create tree model to store sim data items and connect it to views self.simDataItemTreeModel = SimDataItemTreeModel() self.bdTableModel = BdTableModel() self.bdUserGeneratedTableModel = BdUserGeneratedCurvesTableModel() self.simDataItemTreeView.setModel(self.simDataItemTreeModel) self.plotPreview.tableView.setModel(self.bdTableModel) # connect a double clicked section of the bd table to a change of the anchor self.plotPreview.tableView.horizontalHeader( ).sectionDoubleClicked.connect(self.update_bd_table) self.plotPreview.tableView.verticalHeader( ).sectionDoubleClicked.connect( self.update_bd_user_generated_curves_table) # Set custom selection model, so that sub items are automatically # selected if parent is selected self._selection_model = QRecursiveSelectionModel( self.simDataItemTreeView.model()) self.simDataItemTreeView.setSelectionModel(self._selection_model) # Connect list view with model for the selected values of tree view self.selectedSimulationDataItemListModel = OrderedDictModel() self.simDataItemListView.setModel( self.selectedSimulationDataItemListModel) self._selection_model.selectionChanged.connect(self.change_list) # set up signals and slots self.selectedSimulationDataItemListModel.items_changed.connect( self.update_variable_tree) # Connect signals of menus self.actionOpen_File.triggered.connect( self.simDataItemTreeView.add_file) self.actionOpen_Directory.triggered.connect( self.simDataItemTreeView.add_folder) self.actionOpen_Directory_List.triggered.connect( self.simDataItemTreeView.add_folder_list) self.actionHide_PlotSettings.triggered.connect( self.set_plot_settings_visibility) self.actionHide_Sequence.triggered.connect( self.set_sequence_widget_visibility) self.actionHide_Status.triggered.connect( self.set_status_widget_visibility) self.actionSave_Table.triggered.connect(self.save_bd_table) self.actionExport_Figure_as_Tikzpicture.triggered.connect( self.plotPreview.export_plot_tikz) self.actionExport_TableWidget.triggered.connect( self.export_table_to_csv) self.actionSave_Data.triggered.connect(self.save_current_selection) self.action_About.triggered.connect(self.open_about_page) self.variableTreeModel = VariableTreeModel() self.variableTreeView.setModel(self.variableTreeModel) self.plotsettings.visibilityChanged.connect( self.plot_settings_visibility_changed) self.sequenceWidget.visibilityChanged.connect( self.sequence_widget_visibility_changed) self.statusWidget.visibilityChanged.connect( self.status_widget_visibility_changed) # Set recursive selection model for variable view self._variable_tree_selection_model = QRecursiveSelectionModel( self.variableTreeView.model()) self.variableTreeView.setSelectionModel( self._variable_tree_selection_model) # set up combo boxes for rate/psnr and interpolation options self.combo_interp.addItems(["pchip", "pol"]) self.combo_rate_psnr.addItems(["drate", "dsnr"]) self.combo_interp.currentIndexChanged.connect(self.on_combo_box) self.combo_rate_psnr.currentIndexChanged.connect(self.on_combo_box) # set up bd plot checkbox self.checkBox_bdplot.stateChanged.connect(self.update_bd_plot) self.curveWidget.hide() self.curveListModel = OrderedDictModel() self.curveListView.setModel(self.curveListModel) self.curveListSelectionModel = QItemSelectionModel(self.curveListModel) self.curveListView.setSelectionModel(self.curveListSelectionModel) self.curveListView.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.curveWidget.visibilityChanged.connect( self.curve_widget_visibility_changed) self.actionGenerate_curve.triggered.connect(self.generate_new_curve) self.actionRemove_items.triggered.connect(self.remove) self.actionReload_files.triggered.connect(self.reload_files) self.settings = QSettings() self.get_recent_files() self.simDataItemTreeView.itemsOpened.connect(self.add_recent_files) self.watcher = QFileSystemWatcher(self) self.watcher.fileChanged.connect(self.warning_file_change) self.watcher.directoryChanged.connect(self.warning_file_change) self.simDataItemTreeView.parserThread.newParsedData.connect( self.add_files_to_watcher) self.show_file_changed_message = True self.reset_timer = QTimer(self) self.reset_timer.setSingleShot(True) self.reset_timer.setInterval(15000) self.reset_timer.timeout.connect(self._reset_file_changed_message) self.simDataItemTreeView.customContextMenuRequested.connect( self.show_sequences_context_menu) # self.curveListView.actionCalculateBD.triggered.connect(self.bd_user_generated_curves) # sets Visibility for the Plotsettings Widget def set_plot_settings_visibility(self): self.plotsettings.visibilityChanged.disconnect( self.plot_settings_visibility_changed) if self.plotsettings.isHidden(): self.plotsettings.setVisible(True) else: self.plotsettings.setHidden(True) self.plotsettings.visibilityChanged.connect( self.plot_settings_visibility_changed) # updates the QAction if Visibility is changed def plot_settings_visibility_changed(self): if self.plotsettings.isHidden(): self.actionHide_PlotSettings.setChecked(True) else: self.actionHide_PlotSettings.setChecked(False) self._variable_tree_selection_model.selectionChanged.connect( self.update_plot) self.curveListSelectionModel.selectionChanged.connect(self.update_plot) self.simDataItemTreeView.deleteKey.connect(self.remove) # sets Visibility for the Sequence Widget def set_sequence_widget_visibility(self): self.sequenceWidget.visibilityChanged.disconnect( self.sequence_widget_visibility_changed) if self.sequenceWidget.isHidden(): self.sequenceWidget.setVisible(True) else: self.sequenceWidget.setHidden(True) self.sequenceWidget.visibilityChanged.connect( self.sequence_widget_visibility_changed) def sequence_widget_visibility_changed(self): if self.sequenceWidget.isHidden(): self.actionHide_Sequence.setChecked(True) else: self.actionHide_Sequence.setChecked(False) # Sets Visibility for the Status Widget def set_status_widget_visibility(self): self.statusWidget.visibilityChanged.disconnect( self.status_widget_visibility_changed) if self.statusWidget.isHidden(): self.statusWidget.setVisible(True) else: self.statusWidget.setHidden(True) self.statusWidget.visibilityChanged.connect( self.status_widget_visibility_changed) def status_widget_visibility_changed(self): if self.statusWidget.isHidden(): self.actionHide_Status.setChecked(True) else: self.actionHide_Status.setChecked(False) def curve_widget_visibility_changed(self): if self.curveWidget.isHidden(): self.curveListView.delete_key.disconnect() else: self.curveListView.delete_key.connect(self.remove_curves) def remove(self): values = self.selectedSimulationDataItemListModel.values() for value in values: self.watcher.removePath(value.path) # List call necessary to avoid runtime error because of elements changing # during iteration self._variable_tree_selection_model.selectionChanged.disconnect() # disconnect slot to avoid multiple function triggers by selectionChanged signal # not disconnecting slows program down significantly self._selection_model.selectionChanged.disconnect(self.change_list) self.simDataItemTreeModel.remove(list(values)) self.change_list(QItemSelection(), QItemSelection()) self._selection_model.selectionChanged.connect(self.change_list) self._variable_tree_selection_model.selectionChanged.connect( self.update_plot) if len(self.selectedSimulationDataItemListModel.values()) == 0: self.update_plot() def change_list(self, q_selected, q_deselected): """Extend superclass behavior by automatically adding the values of all selected items in :param: `q_selected` to value list model. """ selected_q_indexes = q_deselected.indexes() q_reselect_indexes = [] for q_index in self.simDataItemTreeView.selectedIndexes(): if q_index not in selected_q_indexes: q_reselect_indexes.append(q_index) # Find all all values that are contained by selected tree items tuples = [] for q_index in q_selected.indexes() + q_reselect_indexes: # Add values, ie. sim data items stored at the item, to the list # model. sim_data_items = q_index.internalPointer().values tuples.extend((e.path, e) for e in sim_data_items) # Overwrite all elements in dictionary by selected values # Note, that overwriting only issues one `updated` signal, and thus, # only rerenders the plots one time. Therefore, simply overwriting # is much more efficient, despite it would seem, that selectively # overwriting keys is. self.selectedSimulationDataItemListModel.clear_and_update_from_tuples( tuples) def get_selected_simulation_data_items(self): return [ self.selectedSimulationDataItemListModel[key] for key in self.selectedSimulationDataItemListModel ] def get_plot_data_collection_from_selected_variables(self): """Get a :class: `dict` with y-variable as key and values as items from the current selection of the variable tree. :rtype: :class: `dict` of :class: `string` and :class: `list` """ plot_data_collection = [] for q_index in self.variableTreeView.selectedIndexes(): item = q_index.internalPointer() if len(item.values) > 0: plot_data_collection.extend(item.values) return plot_data_collection def update_variable_tree(self): """Collect all SimDataItems currently selected, and create variable tree and corresponding data from it. Additionaly reselect all previously selected variables. """ # Create a list of all currently selected paths # Note, that *q_index* can not be used directly, because it might # change if the variable tree is recreated selected_path_collection = [] for q_index in self.variableTreeView.selectedIndexes(): selected_path_collection.append( # Create list of identifiers from path # Note, that the root node is excluded from the path [ item.identifier for item in q_index.internalPointer().path[1:] ]) # Join the data of all currently selected items to a dictionary # tree sim_data_items = self.get_selected_simulation_data_items() dict_tree = dict_tree_from_sim_data_items(sim_data_items) # check if qp values are the same # self.check_qp(sim_data_items) # Reset variable tree and update it with *dict_tree* self.variableTreeModel.clear_and_update_from_dict_tree(dict_tree) # Auto expand variable tree self.variableTreeView.expandToDepth(1) # Reselect all variables, which also exist on the new tree for path in selected_path_collection: # Try to reselect, and do nothing, if path does not exist anymore try: # Reselect new item corresponding to the previously selected # path item = self.variableTreeModel.get_item_from_path(*path) self.variableTreeView.selectionModel().select( self.variableTreeModel._get_index_from_item(item), QItemSelectionModel.Select, ) except KeyError: pass # TODO: it might be that some log files do not have a QP value, therefore the check_qp method must be # implemented in a way that these files are not affected # def check_qp(self, sim_data_items): # check if qp values are the same for each sequence # if len(sim_data_items) < 2: return # sim_data_items.sort(key=lambda item: (item.sequence)) # # qp_list,list = [],[] # seq = sim_data_items[0].sequence # config = sim_data_items[0].config # # for item in sim_data_items: # if ((seq == item.sequence) & (config == item.config)): # list.append(item.qp) # elif (seq == item.sequence): #same sequence different config # config = item.config # qp_list.append(list) # list = [] # list.append(item.qp) # else: # different sequence # seq = item.sequence # config = item.config # qp_list.append(list) # if not(all(list == qp_list[0] for list in qp_list)): # QtWidgets.QMessageBox.warning(self, "Warning", # "Be careful! You chose a sequence with different QP.") # return # list, qp_list = [], [] # list.append(item.qp) # qp_list.append(list) # # if not(all(list == qp_list[0] for list in qp_list )): # QtWidgets.QMessageBox.warning(self, "Warning", # "Be careful! You chose a sequence with different QP.") def check_labels(self): selectionmodel = self.variableTreeView.selectionModel() selected = self.variableTreeView.selectedIndexes() # return if no comparison needed if len(selected) < 2: return labelx = [] labely = [] for index in selected: x = index.internalPointer() if len(x.values) > 0: labelx.append(x.values[0].label[0]) labely.append(x.values[0].label[1]) if all(x == labelx[0] for x in labelx) and all(x == labely[0] for x in labely): return else: QtWidgets.QMessageBox.information( self, "Error!", "You should not choose curves with different units.") selectionmodel.clearSelection() # updates the plot if the plot variable is changed def update_plot(self): # user-generated curves and curves loaded from files are not supposed to be mixed user_generated_curves = False if self.sender() == self._variable_tree_selection_model or self.sender( ) == self.curveListSelectionModel: self.check_labels() data_collection = self.get_plot_data_collection_from_selected_variables( ) data_collection_user_generated = [] for index in self.curveListView.selectedIndexes(): data_collection_user_generated.append( self.curveListModel[index.data()]) else: return plot_data_collection = data_collection + data_collection_user_generated if len(data_collection_user_generated): self.plotPreview.tableView.setModel(self.bdUserGeneratedTableModel) self.plotPreview.change_plot(plot_data_collection, True) else: self.plotPreview.tableView.setModel(self.bdTableModel) self.update_table(data_collection) self.plotPreview.change_plot(plot_data_collection, False) if len(data_collection) and len(data_collection_user_generated): # don't mix user-generated and normal curves self.plotPreview.tableView.hide() self.plotPreview.label_warning.show() return self.plotPreview.tableView.show() self.plotPreview.label_warning.hide() self.plotPreview.tableView.model().update( plot_data_collection, self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), not (self.checkBox_bdplot.isChecked())) def get_table_header(self, plot_data_collection): tmp_legend = [] tmp_config = [] # make legend for plot_data in plot_data_collection: tmp = [] for identifiers in plot_data.identifiers[1:]: tmp += identifiers.split(sep) tmp2 = tmp + plot_data.path tmp_legend.append(tmp2) tmp_config.append(tmp) legend = [] config = [] for c in tmp_legend: result = list( filter(lambda x: all(x in l for l in tmp_legend) == False, c)) if result == []: result = [plot_data.path[-1]] legend.append(" ".join(result)) if len(tmp_legend) == 1: legend = [plot_data.path[-1]] #make config for c in tmp_config: result = list( filter(lambda x: all(x in l for l in tmp_config) == False, c)) if ((set([" ".join(result)]) - set(config) != set()) & (result != [])): config.append(" ".join(result)) result = (legend, config) return result #updates the table def update_table(self, plot_data_collection): self.tableWidget.clear() self.tableWidget.setColumnCount(0) self.tableWidget.setRowCount(0) if plot_data_collection != []: if 'Temporal' in plot_data_collection[0].path: self.change_table_temporal(plot_data_collection) else: self.change_table_summary(plot_data_collection) self.tableWidget.resizeColumnsToContents() def change_table_temporal(self, plot_data_collect): plot_data_collection = plot_data_collect self.tableWidget.setRowCount(len(plot_data_collection)) plot_count = data_count = 0 data_names = [] plot_data_collection.sort( key=lambda plot_data: (plot_data.identifiers)) legend = self.get_table_header(plot_data_collection) header = legend[0] for plot_data in plot_data_collection: values = ((float(x), float(y)) for (x, y) in plot_data.values) sorted_value_pairs = sorted(values, key=lambda pair: pair[0]) [xs, ys] = list(zip(*sorted_value_pairs)) # make header if plot_data.identifiers[0] not in data_names: self.tableWidget.insertRow(plot_count) v_item = QtWidgets.QTableWidgetItem( str(plot_data.identifiers[0])) font = self.tableWidget.font() v_item.setData( 6, QtGui.QFont(self.tableWidget.font().setBold(True))) v_item.setData(6, QtGui.QFont("Ubuntu", 11, QtGui.QFont.Bold)) self.tableWidget.setVerticalHeaderItem(plot_count, v_item) header_count = plot_count data_names.append(plot_data.identifiers[0]) plot_count += 1 # round data if plot_data.label[1] == 'dB': ys = tuple(map(lambda i: round(i, 1), ys)) self.tableWidget.horizontalHeader().setVisible(False) # fill up column per column for column_count in range(0, len(xs)): self.tableWidget.setCurrentCell(plot_count, column_count) if column_count > self.tableWidget.currentColumn(): self.tableWidget.insertColumn(column_count) self.tableWidget.setItem( plot_count, column_count, QtWidgets.QTableWidgetItem(str(ys[column_count]))) self.tableWidget.setVerticalHeaderItem( plot_count, QtWidgets.QTableWidgetItem( str(header[data_count]) + " [" + str(plot_data.label[1]) + "] ")) new_item = QtWidgets.QTableWidgetItem(str(xs[column_count])) new_item.setData(6, QtGui.QFont("Ubuntu", 11, QtGui.QFont.Bold)) self.tableWidget.setItem(header_count, column_count, new_item) column_count += 1 plot_count += 1 data_count += 1 def change_table_summary(self, plot_data_collect): plot_data_collection = plot_data_collect header_count = plot_count = data_count = config_count = column_saver = 0 data_names = [] plot_data_collection.sort(key=lambda plot_data: plot_data.path[-1]) plot_data_collection.sort( key=lambda plot_data: plot_data.identifiers[0]) legend = self.get_table_header(plot_data_collection) header = legend[0] config = legend[1] if ((config == []) | (len(config) == 1)): self.change_table_temporal(plot_data_collection) return self.tableWidget.setRowCount(len(plot_data_collection) / len(config)) for plot_data in plot_data_collection: values = ((float(x), float(y)) for (x, y) in plot_data.values) sorted_value_pairs = sorted(values, key=lambda pair: pair[0]) [xs, ys] = list(zip(*sorted_value_pairs)) # make header, important if more than one plot if plot_data.identifiers[0] not in data_names: self.tableWidget.insertRow(plot_count) v_item = QtWidgets.QTableWidgetItem( str(plot_data.identifiers[0])) v_item.setData(6, QtGui.QFont("Ubuntu", 11, QtGui.QFont.Bold)) self.tableWidget.setVerticalHeaderItem(plot_count, v_item) header_count = plot_count data_names.append(plot_data.identifiers[0]) plot_count += 1 # round data if plot_data.label[1] == 'dB': ys = tuple(map(lambda i: round(i, 1), ys)) #horizontal header if more than one config if len(config) > 1: self.tableWidget.horizontalHeader().setVisible(True) else: self.tableWidget.horizontalHeader().setVisible(False) for column_count in range(0, len(xs)): columns = column_saver + column_count if (((column_saver + column_count) >= self.tableWidget.columnCount()) | (self.tableWidget.columnCount() == 0)): self.tableWidget.insertColumn(column_saver + column_count) if plot_count >= self.tableWidget.rowCount(): self.tableWidget.insertRow(plot_count) # units in first row of table new_item = QtWidgets.QTableWidgetItem(plot_data.label[0] + ' | ' + plot_data.label[1]) new_item.setData(6, QtGui.QFont("Ubuntu", 11, QtGui.QFont.Bold)) self.tableWidget.setItem(header_count, column_saver + column_count, new_item) #self.tableWidget.setItem(header_count, column_saver + column_count, QtWidgets.QTableWidgetItem(plot_data.label[0] + ' | ' + plot_data.label[1])) # x and y-value in one cell self.tableWidget.setItem( plot_count, columns, QtWidgets.QTableWidgetItem( str(xs[column_count]) + ' | ' + str(ys[column_count]))) # header self.tableWidget.setHorizontalHeaderItem( column_saver + column_count, QtWidgets.QTableWidgetItem(str(config[config_count]))) column_count += 1 if config[config_count] == header[data_count]: header[data_count] = header[data_count].replace( config[config_count], plot_data.path[-1]) elif config[config_count] in header[data_count]: header[data_count] = header[data_count].replace( config[config_count], '') self.tableWidget.setVerticalHeaderItem( plot_count, QtWidgets.QTableWidgetItem(str(header[data_count]))) column_saver = column_saver + column_count config_count += 1 if config_count == len(config): plot_count += 1 column_saver = config_count = 0 data_count += 1 def update_bd_table(self, index): # update bd table, the index determines the anchor, # if it is non integer per default the first config is regarded as # anchor self.bdTableModel.update_table(self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), index, not (self.checkBox_bdplot.isChecked())) def update_bd_user_generated_curves_table(self, index): clicked_text = self.bdUserGeneratedTableModel.headerData( index, Qt.Vertical, Qt.DisplayRole) self.bdUserGeneratedTableModel.update( None, self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), not (self.checkBox_bdplot.isChecked()), clicked_text) def update_bd_plot(self): data_collection = self.get_plot_data_collection_from_selected_variables( ) data_collection_user_generated = [] for index in self.curveListSelectionModel.selectedIndexes(): data_collection_user_generated.append( self.curveListModel[index.data()]) if len(data_collection): self.bdTableModel.update(data_collection, self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), not (self.checkBox_bdplot.isChecked())) elif len(data_collection_user_generated): self.bdTableModel.update(data_collection_user_generated, self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), not (self.checkBox_bdplot.isChecked())) def export_table_to_csv(self): # remember that the decimal mark is '.' if self.tableWidget.rowCount() > 0: path, extension = QtWidgets.QFileDialog.getSaveFileName( self, 'Save Table View as', '.', 'CSV (*.csv)') if path != '': if '.csv' not in path: path += '.csv' with open(str(path), 'w', newline='') as stream: writer = csv.writer(stream) for row in range(self.tableWidget.rowCount()): rowdata = [] rowdata.append( str( self.tableWidget.verticalHeaderItem(row).data( 0))) #data(0) = data(Qt.displayRole) for column in range(self.tableWidget.columnCount()): item = self.tableWidget.item(row, column) if item is not None: rowdata.append(str(item.text())) else: rowdata.append('') writer.writerow(rowdata) def save_bd_table(self): if self.bdTableModel.rowCount(self) == 0: return filename, extension = QtWidgets.QFileDialog.getSaveFileName( self, 'Save Table as', '.', 'Latex (*.tex)') if filename != '': if '.tex' not in filename: filename += '.tex' self.bdTableModel.export_to_latex(filename) def on_combo_box(self): # just update the bd table but do not change the anchor self.update_bd_table(-1) def save_current_selection(self): """Saves the current selected sim data item collection""" if not self.get_selected_simulation_data_items(): msg = QtWidgets.QMessageBox(self) # use self as parent here msg.setIcon(QtWidgets.QMessageBox.Information) msg.setText( "You did not select any simulation data item to store\n" "Please make a selection and try again.") msg.setWindowTitle("Info") msg.show() return filename, extension = QtWidgets.QFileDialog.getSaveFileName( self, 'Save RD data as', '.', 'RDPlot (*.rd)') if filename != '': if '.rd' not in filename: filename += '.rd' f = open(filename, 'w') f.write( jsonpickle.encode(self.get_selected_simulation_data_items())) f.close() def process_cmd_line_args(self, args): """Processes cmd line arguments. Those are only pathes or files.""" for path in args[1:]: if not isdir(path) and not isfile(path): continue if path.endswith('.rd'): f = open(path, 'r') json_str = f.read() sim_data_items = jsonpickle.decode(json_str) self.simDataItemTreeModel.update(sim_data_items, False) f.close() continue self.simDataItemTreeView.msg.show() self.simDataItemTreeView.parserThread.add_path(path) self.simDataItemTreeView.parserThread.start() def open_about_page(self): """Opens and displays an Html About file""" try: html_path = path.abspath(here + '/docs/about.html') html_file = open(html_path, 'r', encoding='utf-8', errors='ignore') source_code = html_file.read() try: f = open(here + '/version.txt', 'r') app_version = f.readline() except: app_version = 'could not detect version' source_code = source_code.replace("##VERSION##", app_version) source_code = source_code.replace("##here##", here) about_dialog = QtWidgets.QDialog(self) about_dialog.setWindowTitle("About RDPlot") about_dialog.setMaximumSize(950, 800) about_text = QtWidgets.QTextBrowser(about_dialog) about_text.setMinimumWidth(950) about_text.setMinimumHeight(800) about_text.setHtml(source_code) about_text.setOpenExternalLinks(True) about_text.show() about_dialog.exec_() about_dialog.close() about_text.close() except IOError: html_error = QtWidgets.QMessageBox() html_error.setIcon(QtWidgets.QMessageBox.Critical) html_error.setText("Error opening about or help") html_error.setInformativeText( "The html file from the resource could not be loaded.") html_error.exec_() def generate_new_curve(self): plot_data_collection = self.get_plot_data_collection_from_selected_variables( ) if plot_data_collection: new_plot_values = [] for _plot_data in plot_data_collection: new_plot_values.extend(_plot_data.values) if len(new_plot_values) < 4: QtWidgets.QMessageBox.warning( self, "Warning!", "You didn't select at least 4 points.") else: curve_name, ok = QtWidgets.QInputDialog.getText( self, "New curve", "Please enter a name for the new curve.\n" "If you enter an already existing name,\nits data will be overwritten." ) curve_name = curve_name.strip() if curve_name is not '': new_plot_data = PlotData([curve_name], new_plot_values, [], plot_data_collection[0].label) self.add_curve(curve_name, new_plot_data) else: QtWidgets.QMessageBox.warning( self, "Warning!", "Please enter a valid name.") else: QtWidgets.QMessageBox.warning( self, "Warning!", "You didn't select at least 4 points.") def add_curve(self, name, data): if self.curveWidget.isHidden(): self.curveWidget.show() data_tuple = (name, data) self.curveListModel.update_from_tuples((data_tuple, )) # all_indexes = QItemSelection(self.curveListModel.index(0), # self.curveListModel.index(self.curveListModel.rowCount(QModelIndex()))) # self.curveListSelectionModel.select(all_indexes, QItemSelectionModel.Clear) # self.curveListSelectionModel.select(self.curveListModel.index(self.curveListModel.rowCount(QModelIndex())-1), # QItemSelectionModel.Select) # self.curveListView.setFocus() def remove_curves(self): # todo integrate bjontegaard for generated curves(should be fully functional already) curves_to_remove = [] for index in self.curveListSelectionModel.selectedIndexes(): curves_to_remove.append(index.data()) self.curveListModel.remove_keys(curves_to_remove) if len(self.curveListModel) > 0: self.curveListSelectionModel.select(self.curveListModel.index(0), QItemSelectionModel.Select) else: self.curveWidget.hide() self.update_plot() def get_recent_files(self): recent_files = self.settings.value('recentFiles') if recent_files is not None: for recent_file in recent_files: if path.exists(recent_file): action = self.menuRecent_files.addAction(recent_file) action.triggered.connect(self.open_recent_file) def open_recent_file(self): path_recent = self.sender().text() if path.isdir(path_recent): self.simDataItemTreeView.add_folder(path_recent) else: self.simDataItemTreeView.add_file(path_recent) def add_recent_files(self, files, reload): # files doesn't necessarily have to just be a list of files # it can also be a directory if not reload: recent_files = self.settings.value('recentFiles') if recent_files is None: recent_files = [] for file in files: if file in recent_files: # put our file on top of the list recent_files.remove(file) recent_files.insert(0, file) while len(recent_files) > 5: del recent_files[-1] self.settings.setValue('recentFiles', recent_files) self.menuRecent_files.clear() for recent_file in recent_files: if path.exists(recent_file): action = self.menuRecent_files.addAction(recent_file) action.triggered.connect(self.open_recent_file) def add_files_to_watcher(self, items): for item in items: if isfile(item.path): self.watcher.addPath(item.path) def warning_file_change(self, path_item): # inform user about the fact that one of the loaded files has been changed since the application has started # timer is used to avoid spamming the user when multiple files are deleted in a row # retrieve affected notes and parent nodes # change their style in the tree view to indicate which files are affected if self.show_file_changed_message: self.show_file_changed_message = False self.reset_timer.start() QtWidgets.QMessageBox.warning( self, 'File change', 'One or more of your loaded files have been changed.\n' 'You can choose to reload them.\n' 'Hint: Changed files are greyed out in the sequences widget.') else: self.reset_timer.stop() self.reset_timer.start() affected_notes = [] for leaf in self.simDataItemTreeModel.root.leafs: for value in leaf.values: if value.path == path_item: affected_notes.append(leaf) for node in affected_notes: node_index = self.simDataItemTreeModel._get_index_from_item(node) node.setProperty('needs_reload', 'True') parent = self.simDataItemTreeModel.parent(node_index) level = 0 while parent.isValid() and level < 2: #MAX_LEVEL parent.internalPointer().setProperty('needs_reload', 'True') parent = self.simDataItemTreeModel.parent(parent) level += 1 def _reset_file_changed_message(self): self.show_file_changed_message = True def reload_files(self): # remove all selected files first # reload available files # could possibly limit this to only files that we know have been changed def check_children(parent): if len(parent.children) > 0: for child in parent.children: if not check_children(child): return False parent.setProperty('needs_reload', 'False') return True else: if parent.property('needs_reload') == 'True': return False return True values = self.selectedSimulationDataItemListModel.values() if len(values) == 0: for index in self.simDataItemTreeModel.root.leafs: for sim_data_item in index.values: values.append(sim_data_item) items_to_be_reloaded = [] for value in values: if path.exists(value.path): # reload file items_to_be_reloaded.append(value) self._variable_tree_selection_model.selectionChanged.disconnect() self._selection_model.selectionChanged.disconnect(self.change_list) self.simDataItemTreeModel.remove(values) self.simDataItemTreeView.msg.show() for item in items_to_be_reloaded: self.simDataItemTreeView.add_file(item.path, reload=True) self.change_list(QItemSelection(), QItemSelection()) self._selection_model.selectionChanged.connect(self.change_list) self._variable_tree_selection_model.selectionChanged.connect( self.update_plot) for node in self.simDataItemTreeModel.root.children: # remove grey font color if all changed files have been reloaded # have to check every single item because possible deletion of older nodes makes things very difficult check_children(node) def show_sequences_context_menu(self, position): self.menuEdit.exec(self.simDataItemTreeView.mapToGlobal(position))
class NFile(QObject): """ SIGNALS: @askForSaveFileClosing(QString) @fileClosing(QString) @fileChanged() @willDelete(PyQt_PyObject, PyQt_PyObject) @willOverWrite(PyQt_PyObject, QString, QString) @willMove(Qt_PyQtObject, QString, QString) @willSave(QString, QString) @savedAsNewFile(PyQt_PyObject, QString, QString) @gotAPath(PyQt_PyObject) @willAttachToExistingFile(PyQt_PyObject, QString) """ fileChanged = pyqtSignal() fileRemoved = pyqtSignal() fileReaded = pyqtSignal() willAttachToExistingFile = pyqtSignal('PyQt_PyObject', 'QString') gotAPath = pyqtSignal('PyQt_PyObject') willSave = pyqtSignal('QString', 'QString') willMove = pyqtSignal('PyQt_PyObject', 'QString', 'QString') willOverWrite = pyqtSignal('PyQt_PyObject', 'QString', 'QString') willCopyTo = pyqtSignal('PyQt_PyObject', 'QString', 'QString') willDelete = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject') fileClosing = pyqtSignal('QString', bool) def __init__(self, path=None): """ """ self._file_path = path self.__created = False self.__watcher = None self.__mtime = None super(NFile, self).__init__() if not self._exists(): self.__created = True @property def file_name(self): """"Returns filename of nfile""" file_name = None if self._file_path is None: file_name = translations.TR_NEW_DOCUMENT else: file_name = get_basename(self._file_path) return file_name @property def display_name(self): """Returns a pretty name to be displayed by tabs""" display_name = self.file_name if self._file_path is not None and not self.has_write_permission(): display_name += translations.TR_READ_ONLY return display_name @property def is_new_file(self): return self.__created def file_ext(self): """"Returns extension of nfile""" if self._file_path is None: return '' return get_file_extension(self._file_path) @property def file_path(self): """"Returns file path of nfile""" return self._file_path def start_watching(self): """Create a file system watcher and connect its fileChanged SIGNAL to our _file_changed SLOT""" if self.__watcher is None: self.__watcher = QFileSystemWatcher(self) self.__watcher.fileChanged['const QString&'].connect( self._file_changed) if self._file_path is not None: self.__mtime = os.path.getmtime(self._file_path) self.__watcher.addPath(self._file_path) def _file_changed(self, path): if self._exists(): current_mtime = os.path.getmtime(self._file_path) if current_mtime != self.__mtime: self.__mtime = current_mtime self.fileChanged.emit() # FIXME: for swap file # else: # self.fileRemoved.emit() def has_write_permission(self): if not self._exists(): return True return os.access(self._file_path, os.W_OK) def _exists(self): """ Check if we have been created with a path and if such path exists In case there is no path, we are most likely a new file. """ file_exists = False if self._file_path and os.path.exists(self._file_path): file_exists = True return file_exists def attach_to_path(self, new_path): if os.path.exists(new_path): signal_handler = SignalFlowControl() self.willAttachToExistingFile.emit(signal_handler, new_path) if signal_handler.stopped(): return self._file_path = new_path self.gotAPath.emit(self) return self._file_path def create(self): if self.__created: self.save("") self.__created = False def save(self, content, path=None): """ Write a temporary file with .tnj extension and copy it over the original one. .nsf = Ninja Swap File # FIXME: Where to locate addExtension, does not fit here """ new_path = False if path: self.attach_to_path(path) new_path = True save_path = self._file_path if not save_path: raise NinjaNoFileNameException("I am asked to write a " "file but no one told me where") swap_save_path = "%s.nsp" % save_path # If we have a file system watcher, remove the file path # from its watch list until we are done making changes. if self.__watcher is not None: self.__watcher.removePath(save_path) flags = QIODevice.WriteOnly | QIODevice.Truncate f = QFile(swap_save_path) if settings.use_platform_specific_eol(): flags |= QIODevice.Text if not f.open(flags): raise NinjaIOException(f.errorString()) stream = QTextStream(f) encoding = get_file_encoding(content) if encoding: stream.setCodec(encoding) encoded_stream = stream.codec().fromUnicode(content) f.write(encoded_stream) f.flush() f.close() # SIGNAL: Will save (temp, definitive) to warn folder to do something self.willSave.emit(swap_save_path, save_path) self.__mtime = os.path.getmtime(swap_save_path) shutil.move(swap_save_path, save_path) self.reset_state() # If we have a file system watcher, add the saved path back # to its watch list, otherwise create a watcher and start # watching if self.__watcher is not None: if new_path: # self.__watcher.removePath(self.__watcher.files()[0]) self.__watcher.addPath(self._file_path) else: self.__watcher.addPath(save_path) else: self.start_watching() return self def reset_state(self): """ #FIXE: to have a ref to changed I need to have the doc here """ self.__created = False def read(self, path=None): """ Read the file or fail """ open_path = path and path or self._file_path self._file_path = open_path if not self._file_path: raise NinjaNoFileNameException("I am asked to read a file " "but no one told me from where") try: with open(open_path, 'r') as f: content = f.read() except (IOError, UnicodeDecodeError) as reason: raise NinjaIOException(reason) self.fileReaded.emit() return content def move(self, new_path): """ Phisically move the file """ if self._exists(): signal_handler = SignalFlowControl() # SIGNALL: WILL MOVE TO, to warn folder to exist self.willMove.emit(signal_handler, self._file_path, new_path) if signal_handler.stopped(): return if os.path.exists(new_path): signal_handler = SignalFlowControl() self.willOverWrite.emit(signal_handler, self._file_path, new_path) if signal_handler.stopped(): return if self.__watcher is not None: self.__watcher.removePath(self._file_path) shutil.move(self._file_path, new_path) if self.__watcher: self.__watcher.addPath(new_path) self._file_path = new_path def copy(self, new_path): """ Copy the file to a new path """ if self._exists(): signal_handler = SignalFlowControl() # SIGNALL: WILL COPY TO, to warn folder to exist self.willCopyTo.emit(signal_handler, self._file_path, new_path) if signal_handler.stopped(): return if os.path.exists(new_path): signal_handler = SignalFlowControl() self.willOverWrite.emit(signal_handler, self._file_path, new_path) if signal_handler.stopped(): return shutil.copy(self._file_path, new_path) def delete(self, force=False): """ This deletes the object and closes the file. """ # if created but exists this file migth to someone else self.close() if ((not self.__created) or force) and self._exists(): DEBUG("Deleting our own NFile %s" % self._file_path) signal_handler = SignalFlowControl() self.willDelete.emit(signal_handler, self) if not signal_handler.stopped(): if self.__watcher is not None: self.__watcher.removePath(self._file_path) os.remove(self._file_path) def close(self, force_close=False): """ Lets let people know we are going down so they can act upon As you can see close does nothing but let everyone know that we are not saved yet """ DEBUG("About to close NFile") self.fileClosing.emit(self._file_path, force_close) def remove_watcher(self): if self.__watcher is not None: self.__watcher.removePath(self._file_path)
class ReTextWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.initConfig() self.resize(950, 700) screenRect = QDesktopWidget().screenGeometry() if globalSettings.windowGeometry: self.restoreGeometry(globalSettings.windowGeometry) else: self.move((screenRect.width()-self.width())/2, (screenRect.height()-self.height())/2) if not screenRect.contains(self.geometry()): self.showMaximized() if globalSettings.iconTheme: QIcon.setThemeName(globalSettings.iconTheme) if QFile.exists(icon_path+'retext.png'): self.setWindowIcon(QIcon(icon_path+'retext.png')) elif QFile.exists('/usr/share/pixmaps/retext.png'): self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png')) else: self.setWindowIcon(QIcon.fromTheme('retext', QIcon.fromTheme('accessories-text-editor'))) self.editBoxes = [] self.previewBoxes = [] self.highlighters = [] self.markups = [] self.fileNames = [] self.actionPreviewChecked = [] self.actionLivePreviewChecked = [] self.tabWidget = QTabWidget(self) self.initTabWidget() self.setCentralWidget(self.tabWidget) self.tabWidget.currentChanged.connect(self.changeIndex) self.tabWidget.tabCloseRequested.connect(self.closeTab) toolBar = QToolBar(self.tr('File toolbar'), self) self.addToolBar(Qt.TopToolBarArea, toolBar) self.editBar = QToolBar(self.tr('Edit toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.editBar) self.searchBar = QToolBar(self.tr('Search toolbar'), self) self.addToolBar(Qt.BottomToolBarArea, self.searchBar) toolBar.setVisible(not globalSettings.hideToolBar) self.editBar.setVisible(not globalSettings.hideToolBar) self.actionNew = self.act(self.tr('New'), 'document-new', self.createNew, shct=QKeySequence.New) self.actionNew.setPriority(QAction.LowPriority) self.actionOpen = self.act(self.tr('Open'), 'document-open', self.openFile, shct=QKeySequence.Open) self.actionOpen.setPriority(QAction.LowPriority) self.actionSetEncoding = self.act(self.tr('Set encoding'), trig=self.showEncodingDialog) self.actionSetEncoding.setEnabled(False) self.actionReload = self.act(self.tr('Reload'), 'view-refresh', trig=self.openFileMain) self.actionReload.setEnabled(False) self.actionSave = self.act(self.tr('Save'), 'document-save', self.saveFile, shct=QKeySequence.Save) self.actionSave.setEnabled(False) self.actionSave.setPriority(QAction.LowPriority) self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as', self.saveFileAs, shct=QKeySequence.SaveAs) self.actionNextTab = self.act(self.tr('Next tab'), 'go-next', lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown) self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous', lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp) self.actionPrint = self.act(self.tr('Print'), 'document-print', self.printFile, shct=QKeySequence.Print) self.actionPrint.setPriority(QAction.LowPriority) self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview', self.printPreview) self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml) self.actionChangeFont = self.act(self.tr('Change default font'), trig=self.changeFont) self.actionSearch = self.act(self.tr('Find text'), 'edit-find', shct=QKeySequence.Find) self.actionSearch.setCheckable(True) self.actionSearch.triggered[bool].connect(self.searchBar.setVisible) self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged) self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E, trigbool=self.preview) if QIcon.hasThemeIcon('document-preview'): self.actionPreview.setIcon(QIcon.fromTheme('document-preview')) elif QIcon.hasThemeIcon('preview-file'): self.actionPreview.setIcon(QIcon.fromTheme('preview-file')) elif QIcon.hasThemeIcon('x-office-document'): self.actionPreview.setIcon(QIcon.fromTheme('x-office-document')) else: self.actionPreview.setIcon(QIcon(icon_path+'document-preview.png')) self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L, trigbool=self.enableLivePreview) self.actionTableMode = self.act(self.tr('Table mode'), shct=Qt.CTRL+Qt.Key_T, trigbool=lambda x: self.editBoxes[self.ind].enableTableMode(x)) if ReTextFakeVimHandler: self.actionFakeVimMode = self.act(self.tr('FakeVim mode'), shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode) if globalSettings.useFakeVim: self.actionFakeVimMode.setChecked(True) self.enableFakeVimMode(True) self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen', shct=Qt.Key_F11, trigbool=self.enableFullScreen) self.actionFullScreen.setPriority(QAction.LowPriority) self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system', trig=self.openConfigDialog) self.actionConfig.setMenuRole(QAction.PreferencesRole) self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml) self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf) self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf) self.getExportExtensionsList() self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit) self.actionQuit.setMenuRole(QAction.QuitRole) self.actionQuit.triggered.connect(self.close) self.actionUndo = self.act(self.tr('Undo'), 'edit-undo', lambda: self.editBoxes[self.ind].undo(), shct=QKeySequence.Undo) self.actionRedo = self.act(self.tr('Redo'), 'edit-redo', lambda: self.editBoxes[self.ind].redo(), shct=QKeySequence.Redo) self.actionCopy = self.act(self.tr('Copy'), 'edit-copy', lambda: self.editBoxes[self.ind].copy(), shct=QKeySequence.Copy) self.actionCut = self.act(self.tr('Cut'), 'edit-cut', lambda: self.editBoxes[self.ind].cut(), shct=QKeySequence.Cut) self.actionPaste = self.act(self.tr('Paste'), 'edit-paste', lambda: self.editBoxes[self.ind].paste(), shct=QKeySequence.Paste) self.actionUndo.setEnabled(False) self.actionRedo.setEnabled(False) self.actionCopy.setEnabled(False) self.actionCut.setEnabled(False) qApp = QApplication.instance() qApp.clipboard().dataChanged.connect(self.clipboardDataChanged) self.clipboardDataChanged() if enchant_available: self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck) self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale) self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit) self.actionWebKit.setChecked(globalSettings.useWebKit) self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir) self.actionFind = self.act(self.tr('Next'), 'go-next', self.find, shct=QKeySequence.FindNext) self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous', lambda: self.find(back=True), shct=QKeySequence.FindPrevious) self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp) self.aboutWindowTitle = self.tr('About %s', 'Example of final string: About ReText') self.aboutWindowTitle = self.aboutWindowTitle % app_name self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog) self.actionAbout.setMenuRole(QAction.AboutRole) self.actionAboutQt = self.act(self.tr('About Qt')) self.actionAboutQt.setMenuRole(QAction.AboutQtRole) self.actionAboutQt.triggered.connect(qApp.aboutQt) availableMarkups = markups.get_available_markups() if not availableMarkups: print('Warning: no markups are available!') self.defaultMarkup = availableMarkups[0] if availableMarkups else None if globalSettings.defaultMarkup: mc = markups.find_markup_class_by_name(globalSettings.defaultMarkup) if mc and mc.available(): self.defaultMarkup = mc if len(availableMarkups) > 1: self.chooseGroup = QActionGroup(self) markupActions = [] for markup in availableMarkups: markupAction = self.act(markup.name, trigbool=self.markupFunction(markup)) if markup == self.defaultMarkup: markupAction.setChecked(True) self.chooseGroup.addAction(markupAction) markupActions.append(markupAction) self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold, trig=lambda: self.insertChars('**')) self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic, trig=lambda: self.insertChars('*')) self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline, trig=lambda: self.insertTag('u')) self.usefulTags = ('a', 'big', 'center', 'img', 's', 'small', 'span', 'table', 'td', 'tr', 'u') self.usefulChars = ('deg', 'divide', 'dollar', 'hellip', 'laquo', 'larr', 'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo', 'rarr', 'rsquo', 'times') self.tagsBox = QComboBox(self.editBar) self.tagsBox.addItem(self.tr('Tags')) self.tagsBox.addItems(self.usefulTags) self.tagsBox.activated.connect(self.insertTag) self.symbolBox = QComboBox(self.editBar) self.symbolBox.addItem(self.tr('Symbols')) self.symbolBox.addItems(self.usefulChars) self.symbolBox.activated.connect(self.insertSymbol) self.updateStyleSheet() menubar = QMenuBar(self) menubar.setGeometry(QRect(0, 0, 800, 25)) self.setMenuBar(menubar) menuFile = menubar.addMenu(self.tr('File')) menuEdit = menubar.addMenu(self.tr('Edit')) menuHelp = menubar.addMenu(self.tr('Help')) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent')) self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles) menuFile.addMenu(self.menuRecentFiles) menuFile.addAction(self.actionShow) menuFile.addAction(self.actionSetEncoding) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addSeparator() menuFile.addAction(self.actionNextTab) menuFile.addAction(self.actionPrevTab) menuFile.addSeparator() menuExport = menuFile.addMenu(self.tr('Export')) menuExport.addAction(self.actionSaveHtml) menuExport.addAction(self.actionOdf) menuExport.addAction(self.actionPdf) if self.extensionActions: menuExport.addSeparator() for action, mimetype in self.extensionActions: menuExport.addAction(action) menuExport.aboutToShow.connect(self.updateExtensionsVisibility) menuFile.addAction(self.actionPrint) menuFile.addAction(self.actionPrintPreview) menuFile.addSeparator() menuFile.addAction(self.actionQuit) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addSeparator() if enchant_available: menuSC = menuEdit.addMenu(self.tr('Spell check')) menuSC.addAction(self.actionEnableSC) menuSC.addAction(self.actionSetLocale) menuEdit.addAction(self.actionSearch) menuEdit.addAction(self.actionChangeFont) menuEdit.addSeparator() if len(availableMarkups) > 1: self.menuMode = menuEdit.addMenu(self.tr('Default markup')) for markupAction in markupActions: self.menuMode.addAction(markupAction) menuFormat = menuEdit.addMenu(self.tr('Formatting')) menuFormat.addAction(self.actionBold) menuFormat.addAction(self.actionItalic) menuFormat.addAction(self.actionUnderline) menuEdit.addAction(self.actionWebKit) menuEdit.addSeparator() menuEdit.addAction(self.actionViewHtml) menuEdit.addAction(self.actionLivePreview) menuEdit.addAction(self.actionPreview) menuEdit.addAction(self.actionTableMode) if ReTextFakeVimHandler: menuEdit.addAction(self.actionFakeVimMode) menuEdit.addSeparator() menuEdit.addAction(self.actionFullScreen) menuEdit.addAction(self.actionConfig) menuHelp.addAction(self.actionHelp) menuHelp.addSeparator() menuHelp.addAction(self.actionAbout) menuHelp.addAction(self.actionAboutQt) menubar.addMenu(menuFile) menubar.addMenu(menuEdit) menubar.addMenu(menuHelp) toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) toolBar.addAction(self.actionNew) toolBar.addSeparator() toolBar.addAction(self.actionOpen) toolBar.addAction(self.actionSave) toolBar.addAction(self.actionPrint) toolBar.addSeparator() toolBar.addAction(self.actionPreview) toolBar.addAction(self.actionFullScreen) self.editBar.addAction(self.actionUndo) self.editBar.addAction(self.actionRedo) self.editBar.addSeparator() self.editBar.addAction(self.actionCut) self.editBar.addAction(self.actionCopy) self.editBar.addAction(self.actionPaste) self.editBar.addSeparator() self.editBar.addWidget(self.tagsBox) self.editBar.addWidget(self.symbolBox) self.searchEdit = QLineEdit(self.searchBar) self.searchEdit.setPlaceholderText(self.tr('Search')) self.searchEdit.returnPressed.connect(self.find) self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar) self.searchBar.addWidget(self.searchEdit) self.searchBar.addSeparator() self.searchBar.addWidget(self.csBox) self.searchBar.addAction(self.actionFindPrev) self.searchBar.addAction(self.actionFind) self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.searchBar.setVisible(False) self.autoSaveEnabled = globalSettings.autoSave if self.autoSaveEnabled: timer = QTimer(self) timer.start(60000) timer.timeout.connect(self.saveAll) self.ind = None if enchant_available: self.sl = globalSettings.spellCheckLocale if self.sl: try: enchant.Dict(self.sl) except Exception as e: print(e, file=sys.stderr) self.sl = None if globalSettings.spellCheck: self.actionEnableSC.setChecked(True) self.enableSpellCheck(True) self.fileSystemWatcher = QFileSystemWatcher() self.fileSystemWatcher.fileChanged.connect(self.fileChanged) def initConfig(self): self.font = None if globalSettings.font: self.font = QFont(globalSettings.font) if self.font and globalSettings.fontSize: self.font.setPointSize(globalSettings.fontSize) def updateStyleSheet(self): if globalSettings.styleSheet: sheetfile = QFile(globalSettings.styleSheet) sheetfile.open(QIODevice.ReadOnly) self.ss = QTextStream(sheetfile).readAll() sheetfile.close() else: self.ss = '' def initTabWidget(self): def dragEnterEvent(e): e.acceptProposedAction() def dropEvent(e): fn = bytes(e.mimeData().data('text/plain')).decode().rstrip() if fn.startswith('file:'): fn = QUrl(fn).toLocalFile() self.openFileWrapper(fn) self.tabWidget.setTabsClosable(True) self.tabWidget.setAcceptDrops(True) self.tabWidget.dragEnterEvent = dragEnterEvent self.tabWidget.dropEvent = dropEvent def act(self, name, icon=None, trig=None, trigbool=None, shct=None): if not isinstance(shct, QKeySequence): shct = QKeySequence(shct) if icon: action = QAction(self.actIcon(icon), name, self) else: action = QAction(name, self) if trig: action.triggered.connect(trig) elif trigbool: action.setCheckable(True) action.triggered[bool].connect(trigbool) if shct: action.setShortcut(shct) return action def actIcon(self, name): return QIcon.fromTheme(name, QIcon(icon_path+name+'.png')) def printError(self): import traceback print('Exception occured while parsing document:', file=sys.stderr) traceback.print_exc() def getSplitter(self, index): splitter = QSplitter(Qt.Horizontal) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBoxes[index], self.previewBoxes[index]: widget.setMinimumWidth(125) splitter.addWidget(widget) splitter.setSizes((50, 50)) splitter.setChildrenCollapsible(False) return splitter def getWebView(self): webView = QWebView() if not globalSettings.handleWebLinks: webView.page().setLinkDelegationPolicy(QWebPage.DelegateExternalLinks) webView.page().linkClicked.connect(QDesktopServices.openUrl) webView.settings().setAttribute(QWebSettings.LocalContentCanAccessFileUrls, False) return webView def createTab(self, fileName): self.previewBlocked = False self.editBoxes.append(ReTextEdit(self)) self.highlighters.append(ReTextHighlighter(self.editBoxes[-1].document())) if enchant_available and self.actionEnableSC.isChecked(): self.highlighters[-1].dictionary = \ enchant.Dict(self.sl) if self.sl else enchant.Dict() self.highlighters[-1].rehighlight() if globalSettings.useWebKit: self.previewBoxes.append(self.getWebView()) else: self.previewBoxes.append(QTextBrowser()) self.previewBoxes[-1].setOpenExternalLinks(True) self.previewBoxes[-1].setVisible(False) self.fileNames.append(fileName) markupClass = self.getMarkupClass(fileName) self.markups.append(self.getMarkup(fileName)) self.highlighters[-1].docType = (markupClass.name if markupClass else '') liveMode = globalSettings.restorePreviewState and globalSettings.previewState self.actionPreviewChecked.append(liveMode) self.actionLivePreviewChecked.append(liveMode) metrics = QFontMetrics(self.editBoxes[-1].font()) self.editBoxes[-1].setTabStopWidth(globalSettings.tabWidth * metrics.width(' ')) self.editBoxes[-1].textChanged.connect(self.updateLivePreviewBox) self.editBoxes[-1].undoAvailable.connect(self.actionUndo.setEnabled) self.editBoxes[-1].redoAvailable.connect(self.actionRedo.setEnabled) self.editBoxes[-1].copyAvailable.connect(self.enableCopy) self.editBoxes[-1].document().modificationChanged.connect(self.modificationChanged) if globalSettings.useFakeVim: self.installFakeVimHandler(self.editBoxes[-1]) return self.getSplitter(-1) def closeTab(self, ind): if self.maybeSave(ind): if self.tabWidget.count() == 1: self.tabWidget.addTab(self.createTab(""), self.tr("New document")) if self.fileNames[ind]: self.fileSystemWatcher.removePath(self.fileNames[ind]) del self.editBoxes[ind] del self.previewBoxes[ind] del self.highlighters[ind] del self.markups[ind] del self.fileNames[ind] del self.actionPreviewChecked[ind] del self.actionLivePreviewChecked[ind] self.tabWidget.removeTab(ind) def getMarkupClass(self, fileName=None): if fileName is None: fileName = self.fileNames[self.ind] if fileName: markupClass = markups.get_markup_for_file_name( fileName, return_class=True) if markupClass: return markupClass return self.defaultMarkup def getMarkup(self, fileName=None): if fileName is None: fileName = self.fileNames[self.ind] markupClass = self.getMarkupClass(fileName=fileName) if markupClass and markupClass.available(): return markupClass(filename=fileName) def docTypeChanged(self): oldType = self.highlighters[self.ind].docType markupClass = self.getMarkupClass() newType = markupClass.name if markupClass else '' if oldType != newType: self.markups[self.ind] = self.getMarkup() self.updatePreviewBox() self.highlighters[self.ind].docType = newType self.highlighters[self.ind].rehighlight() dtMarkdown = (newType == DOCTYPE_MARKDOWN) dtMkdOrReST = (newType in (DOCTYPE_MARKDOWN, DOCTYPE_REST)) self.tagsBox.setEnabled(dtMarkdown) self.symbolBox.setEnabled(dtMarkdown) self.actionUnderline.setEnabled(dtMarkdown) self.actionBold.setEnabled(dtMkdOrReST) self.actionItalic.setEnabled(dtMkdOrReST) canReload = bool(self.fileNames[self.ind]) and not self.autoSaveActive() self.actionSetEncoding.setEnabled(canReload) self.actionReload.setEnabled(canReload) def changeIndex(self, ind): if ind > -1: self.actionUndo.setEnabled(self.editBoxes[ind].document().isUndoAvailable()) self.actionRedo.setEnabled(self.editBoxes[ind].document().isRedoAvailable()) self.actionCopy.setEnabled(self.editBoxes[ind].textCursor().hasSelection()) self.actionCut.setEnabled(self.editBoxes[ind].textCursor().hasSelection()) self.actionPreview.setChecked(self.actionPreviewChecked[ind]) self.actionLivePreview.setChecked(self.actionLivePreviewChecked[ind]) self.actionTableMode.setChecked(self.editBoxes[ind].tableModeEnabled) self.editBar.setDisabled(self.actionPreviewChecked[ind]) self.ind = ind if self.fileNames[ind]: self.setCurrentFile() else: self.setWindowTitle(self.tr('New document') + '[*]') self.docTypeChanged() self.modificationChanged(self.editBoxes[ind].document().isModified()) if globalSettings.restorePreviewState: globalSettings.previewState = self.actionLivePreviewChecked[ind] if self.actionLivePreviewChecked[ind]: self.enableLivePreview(True) self.editBoxes[self.ind].setFocus(Qt.OtherFocusReason) def changeFont(self): if not self.font: self.font = QFont() fd = QFontDialog.getFont(self.font, self) if fd[1]: self.font = QFont() self.font.setFamily(fd[0].family()) settings.setValue('font', fd[0].family()) self.font.setPointSize(fd[0].pointSize()) settings.setValue('fontSize', fd[0].pointSize()) self.updatePreviewBox() def preview(self, viewmode): self.actionPreviewChecked[self.ind] = viewmode if self.actionLivePreview.isChecked(): self.actionLivePreview.setChecked(False) return self.enableLivePreview(False) self.editBar.setDisabled(viewmode) self.editBoxes[self.ind].setVisible(not viewmode) self.previewBoxes[self.ind].setVisible(viewmode) if viewmode: self.updatePreviewBox() def enableLivePreview(self, livemode): if globalSettings.restorePreviewState: globalSettings.previewState = livemode self.actionLivePreviewChecked[self.ind] = livemode self.actionPreviewChecked[self.ind] = livemode self.actionPreview.setChecked(livemode) self.editBar.setEnabled(True) self.previewBoxes[self.ind].setVisible(livemode) self.editBoxes[self.ind].setVisible(True) if livemode: self.updatePreviewBox() def enableWebKit(self, enable): globalSettings.useWebKit = enable oldind = self.ind self.tabWidget.clear() for self.ind in range(len(self.editBoxes)): if enable: self.previewBoxes[self.ind] = self.getWebView() else: self.previewBoxes[self.ind] = QTextBrowser() self.previewBoxes[self.ind].setOpenExternalLinks(True) splitter = self.getSplitter(self.ind) self.tabWidget.addTab(splitter, self.getDocumentTitle(baseName=True)) self.updatePreviewBox() self.previewBoxes[self.ind].setVisible(self.actionPreviewChecked[self.ind]) self.ind = oldind self.tabWidget.setCurrentIndex(self.ind) def enableCopy(self, copymode): self.actionCopy.setEnabled(copymode) self.actionCut.setEnabled(copymode) def enableFullScreen(self, yes): if yes: self.showFullScreen() else: self.showNormal() def openConfigDialog(self): dlg = ConfigDialog(self) dlg.setWindowTitle(self.tr('Preferences')) dlg.show() def installFakeVimHandler(self, editor): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(editor, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) self.actionFakeVimMode.triggered.connect(fakeVimEditor.remove) def enableFakeVimMode(self, yes): globalSettings.useFakeVim = yes if yes: FakeVimMode.init(self) for editor in self.editBoxes: self.installFakeVimHandler(editor) else: FakeVimMode.exit(self) def enableSpellCheck(self, yes): if yes: if self.sl: self.setAllDictionaries(enchant.Dict(self.sl)) else: self.setAllDictionaries(enchant.Dict()) else: self.setAllDictionaries(None) globalSettings.spellCheck = yes def setAllDictionaries(self, dictionary): for hl in self.highlighters: hl.dictionary = dictionary hl.rehighlight() def changeLocale(self): if self.sl: localedlg = LocaleDialog(self, defaultText=self.sl) else: localedlg = LocaleDialog(self) if localedlg.exec() != QDialog.Accepted: return sl = localedlg.localeEdit.text() setdefault = localedlg.checkBox.isChecked() if sl: try: sl = str(sl) enchant.Dict(sl) except Exception as e: QMessageBox.warning(self, '', str(e)) else: self.sl = sl self.enableSpellCheck(self.actionEnableSC.isChecked()) else: self.sl = None self.enableSpellCheck(self.actionEnableSC.isChecked()) if setdefault: globalSettings.spellCheckLocale = sl def searchBarVisibilityChanged(self, visible): self.actionSearch.setChecked(visible) if visible: self.searchEdit.setFocus(Qt.ShortcutFocusReason) def find(self, back=False): flags = QTextDocument.FindFlags() if back: flags |= QTextDocument.FindBackward if self.csBox.isChecked(): flags |= QTextDocument.FindCaseSensitively text = self.searchEdit.text() editBox = self.editBoxes[self.ind] cursor = editBox.textCursor() newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) cursor.movePosition(QTextCursor.End if back else QTextCursor.Start) newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) self.setSearchEditColor(False) def setSearchEditColor(self, found): palette = self.searchEdit.palette() palette.setColor(QPalette.Active, QPalette.Base, Qt.white if found else QColor(255, 102, 102)) self.searchEdit.setPalette(palette) def getHtml(self, includeStyleSheet=True, includeTitle=True, includeMeta=False, styleForWebKit=False, webenv=False): if self.markups[self.ind] is None: markupClass = self.getMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '') errMsg = errMsg.replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg text = self.editBoxes[self.ind].toPlainText() headers = '' if includeStyleSheet: fontline = '' if styleForWebKit: fontname = self.font.family() if self.font else 'Sans' fontsize = (self.font if self.font else QFont()).pointSize() fontline = 'body { font-family: %s; font-size: %spt }\n' % \ (fontname, fontsize) headers += '<style type="text/css">\n' + fontline + self.ss + '</style>\n' cssFileName = self.getDocumentTitle(baseName=True)+'.css' if QFile(cssFileName).exists(): headers += '<link rel="stylesheet" type="text/css" href="%s">\n' \ % cssFileName if includeMeta: headers += '<meta name="generator" content="%s %s">\n' % \ (app_name, app_version) fallbackTitle = self.getDocumentTitle() if includeTitle else '' return self.markups[self.ind].get_whole_html(text, custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=fallbackTitle, webenv=webenv) def updatePreviewBox(self): self.previewBlocked = False pb = self.previewBoxes[self.ind] textedit = isinstance(pb, QTextEdit) if textedit: scrollbar = pb.verticalScrollBar() disttobottom = scrollbar.maximum() - scrollbar.value() else: frame = pb.page().mainFrame() scrollpos = frame.scrollPosition() try: html = self.getHtml(styleForWebKit=(not textedit)) except Exception: return self.printError() if textedit: pb.setHtml(html) else: pb.setHtml(html, QUrl.fromLocalFile(self.fileNames[self.ind])) if self.font and textedit: pb.document().setDefaultFont(self.font) if textedit: scrollbar.setValue(scrollbar.maximum() - disttobottom) else: frame.setScrollPosition(scrollpos) def updateLivePreviewBox(self): if self.actionLivePreview.isChecked() and self.previewBlocked == False: self.previewBlocked = True QTimer.singleShot(1000, self.updatePreviewBox) def showInDir(self): if self.fileNames[self.ind]: path = QFileInfo(self.fileNames[self.ind]).path() QDesktopServices.openUrl(QUrl.fromLocalFile(path)) else: QMessageBox.warning(self, '', self.tr("Please, save the file somewhere.")) def setCurrentFile(self): self.setWindowTitle("") self.tabWidget.setTabText(self.ind, self.getDocumentTitle(baseName=True)) self.setWindowFilePath(self.fileNames[self.ind]) files = readListFromSettings("recentFileList") while self.fileNames[self.ind] in files: files.remove(self.fileNames[self.ind]) files.insert(0, self.fileNames[self.ind]) if len(files) > 10: del files[10:] writeListToSettings("recentFileList", files) QDir.setCurrent(QFileInfo(self.fileNames[self.ind]).dir().path()) self.docTypeChanged() def createNew(self, text=None): self.tabWidget.addTab(self.createTab(""), self.tr("New document")) self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if text: self.editBoxes[self.ind].textCursor().insertText(text) def switchTab(self, shift=1): self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count()) def updateRecentFiles(self): self.menuRecentFiles.clear() self.recentFilesActions = [] filesOld = readListFromSettings("recentFileList") files = [] for f in filesOld: if QFile.exists(f): files.append(f) self.recentFilesActions.append(self.act(f, trig=self.openFunction(f))) writeListToSettings("recentFileList", files) for action in self.recentFilesActions: self.menuRecentFiles.addAction(action) def markupFunction(self, markup): return lambda: self.setDefaultMarkup(markup) def openFunction(self, fileName): return lambda: self.openFileWrapper(fileName) def extensionFuntion(self, data): return lambda: \ self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension']) def getExportExtensionsList(self): extensions = [] for extsprefix in datadirs: extsdir = QDir(extsprefix+'/export-extensions/') if extsdir.exists(): for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], QDir.Files | QDir.Readable): extensions.append(self.readExtension(fileInfo.filePath())) locale = QLocale.system().name() self.extensionActions = [] for extension in extensions: try: if ('Name[%s]' % locale) in extension: name = extension['Name[%s]' % locale] elif ('Name[%s]' % locale.split('_')[0]) in extension: name = extension['Name[%s]' % locale.split('_')[0]] else: name = extension['Name'] data = {} for prop in ('FileFilter', 'DefaultExtension', 'Exec'): if 'X-ReText-'+prop in extension: data[prop] = extension['X-ReText-'+prop] elif prop in extension: data[prop] = extension[prop] else: data[prop] = '' action = self.act(name, trig=self.extensionFuntion(data)) if 'Icon' in extension: action.setIcon(self.actIcon(extension['Icon'])) mimetype = extension['MimeType'] if 'MimeType' in extension else None except KeyError: print('Failed to parse extension: Name is required', file=sys.stderr) else: self.extensionActions.append((action, mimetype)) def updateExtensionsVisibility(self): markupClass = self.getMarkupClass() for action in self.extensionActions: if markupClass is None: action[0].setEnabled(False) continue mimetype = action[1] if mimetype == None: enabled = True elif markupClass == markups.MarkdownMarkup: enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown")) elif markupClass == markups.ReStructuredTextMarkup: enabled = (mimetype in ("text/x-retext-rst", "text/x-rst")) else: enabled = False action[0].setEnabled(enabled) def readExtension(self, fileName): extFile = QFile(fileName) extFile.open(QIODevice.ReadOnly) extension = {} stream = QTextStream(extFile) while not stream.atEnd(): line = stream.readLine() if '=' in line: index = line.index('=') extension[line[:index].rstrip()] = line[index+1:].lstrip() extFile.close() return extension def openFile(self): supportedExtensions = ['.txt'] for markup in markups.get_all_markups(): supportedExtensions += markup.file_extensions fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;' fileNames = QFileDialog.getOpenFileNames(self, self.tr("Select one or several files to open"), "", self.tr("Supported files") + fileFilter + self.tr("All files (*)")) for fileName in fileNames[0]: self.openFileWrapper(fileName) def openFileWrapper(self, fileName): if not fileName: return fileName = QFileInfo(fileName).canonicalFilePath() exists = False for i in range(self.tabWidget.count()): if self.fileNames[i] == fileName: exists = True ex = i if exists: self.tabWidget.setCurrentIndex(ex) elif QFile.exists(fileName): noEmptyTab = ( (self.ind is None) or self.fileNames[self.ind] or self.editBoxes[self.ind].toPlainText() or self.editBoxes[self.ind].document().isModified() ) if noEmptyTab: self.tabWidget.addTab(self.createTab(fileName), "") self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if fileName: self.fileSystemWatcher.addPath(fileName) self.fileNames[self.ind] = fileName self.openFileMain() def openFileMain(self, encoding=None): openfile = QFile(self.fileNames[self.ind]) openfile.open(QIODevice.ReadOnly) stream = QTextStream(openfile) if encoding: stream.setCodec(encoding) elif globalSettings.defaultCodec: stream.setCodec(globalSettings.defaultCodec) text = stream.readAll() openfile.close() markupClass = markups.get_markup_for_file_name( self.fileNames[self.ind], return_class=True) self.highlighters[self.ind].docType = (markupClass.name if markupClass else '') self.markups[self.ind] = self.getMarkup() if self.defaultMarkup: self.highlighters[self.ind].docType = self.defaultMarkup.name editBox = self.editBoxes[self.ind] modified = bool(encoding) and (editBox.toPlainText() != text) editBox.setPlainText(text) self.setCurrentFile() editBox.document().setModified(modified) self.setWindowModified(modified) def showEncodingDialog(self): if not self.maybeSave(self.ind): return encoding, ok = QInputDialog.getItem(self, '', self.tr('Select file encoding from the list:'), [bytes(b).decode() for b in QTextCodec.availableCodecs()], 0, False) if ok: self.openFileMain(encoding) def saveFile(self): self.saveFileMain(dlg=False) def saveFileAs(self): self.saveFileMain(dlg=True) def saveAll(self): oldind = self.ind for self.ind in range(self.tabWidget.count()): if self.fileNames[self.ind] and QFileInfo(self.fileNames[self.ind]).isWritable(): self.saveFileCore(self.fileNames[self.ind]) self.editBoxes[self.ind].document().setModified(False) self.ind = oldind def saveFileMain(self, dlg): if (not self.fileNames[self.ind]) or dlg: markupClass = self.getMarkupClass() if (markupClass is None) or not hasattr(markupClass, 'default_extension'): defaultExt = self.tr("Plain text (*.txt)") ext = ".txt" else: defaultExt = self.tr('%s files', 'Example of final string: Markdown files') \ % markupClass.name + ' (' + str.join(' ', ('*'+extension for extension in markupClass.file_extensions)) + ')' if markupClass == markups.MarkdownMarkup: ext = globalSettings.markdownDefaultFileExtension elif markupClass == markups.ReStructuredTextMarkup: ext = globalSettings.restDefaultFileExtension else: ext = markupClass.default_extension newFileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", defaultExt)[0] if newFileName: if not QFileInfo(newFileName).suffix(): newFileName += ext if self.fileNames[self.ind]: self.fileSystemWatcher.removePath(self.fileNames[self.ind]) self.fileNames[self.ind] = newFileName self.actionSetEncoding.setDisabled(self.autoSaveActive()) if self.fileNames[self.ind]: result = self.saveFileCore(self.fileNames[self.ind]) if result: self.setCurrentFile() self.editBoxes[self.ind].document().setModified(False) self.setWindowModified(False) return True else: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return False def saveFileCore(self, fn): self.fileSystemWatcher.removePath(fn) savefile = QFile(fn) result = savefile.open(QIODevice.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBoxes[self.ind].toPlainText() savefile.close() self.fileSystemWatcher.addPath(fn) return result def saveHtml(self, fileName): if not QFileInfo(fileName).suffix(): fileName += ".html" try: htmltext = self.getHtml(includeStyleSheet=False, includeMeta=True, webenv=True) except Exception: return self.printError() htmlFile = QFile(fileName) htmlFile.open(QIODevice.WriteOnly) html = QTextStream(htmlFile) if globalSettings.defaultCodec: html.setCodec(globalSettings.defaultCodec) html << htmltext htmlFile.close() def textDocument(self): td = QTextDocument() td.setMetaInformation(QTextDocument.DocumentTitle, self.getDocumentTitle()) if self.ss: td.setDefaultStyleSheet(self.ss) td.setHtml(self.getHtml()) if self.font: td.setDefaultFont(self.font) return td def saveOdf(self): try: document = self.textDocument() except Exception: return self.printError() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to ODT"), "", self.tr("OpenDocument text files (*.odt)"))[0] if not QFileInfo(fileName).suffix(): fileName += ".odt" writer = QTextDocumentWriter(fileName) writer.setFormat("odf") writer.write(document) def saveFileHtml(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", self.tr("HTML files (*.html *.htm)"))[0] if fileName: self.saveHtml(fileName) def getDocumentForPrint(self): if globalSettings.useWebKit: return self.previewBoxes[self.ind] try: return self.textDocument() except Exception: self.printError() def standardPrinter(self): printer = QPrinter(QPrinter.HighResolution) printer.setDocName(self.getDocumentTitle()) printer.setCreator(app_name+" "+app_version) return printer def savePdf(self): self.updatePreviewBox() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to PDF"), "", self.tr("PDF files (*.pdf)"))[0] if fileName: if not QFileInfo(fileName).suffix(): fileName += ".pdf" printer = self.standardPrinter() printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fileName) document = self.getDocumentForPrint() if document != None: document.print(printer) def printFile(self): self.updatePreviewBox() printer = self.standardPrinter() dlg = QPrintDialog(printer, self) dlg.setWindowTitle(self.tr("Print document")) if (dlg.exec() == QDialog.Accepted): document = self.getDocumentForPrint() if document != None: document.print(printer) def printPreview(self): document = self.getDocumentForPrint() if document == None: return printer = self.standardPrinter() preview = QPrintPreviewDialog(printer, self) preview.paintRequested.connect(document.print) preview.exec() def runExtensionCommand(self, command, filefilter, defaultext): of = ('%of' in command) html = ('%html' in command) if of: if defaultext and not filefilter: filefilter = '*'+defaultext fileName = QFileDialog.getSaveFileName(self, self.tr('Export document'), '', filefilter)[0] if not fileName: return if defaultext and not QFileInfo(fileName).suffix(): fileName += defaultext basename = '.%s.retext-temp' % self.getDocumentTitle(baseName=True) if html: tmpname = basename+'.html' self.saveHtml(tmpname) else: tmpname = basename+self.getMarkupClass().default_extension self.saveFileCore(tmpname) command = command.replace('%of', '"out'+defaultext+'"') command = command.replace('%html' if html else '%if', '"'+tmpname+'"') try: Popen(str(command), shell=True).wait() except Exception as error: errorstr = str(error) QMessageBox.warning(self, '', self.tr('Failed to execute the command:') + '\n' + errorstr) QFile(tmpname).remove() if of: QFile('out'+defaultext).rename(fileName) def getDocumentTitle(self, baseName=False): markup = self.markups[self.ind] realTitle = '' if markup and not baseName: text = self.editBoxes[self.ind].toPlainText() try: realTitle = markup.get_document_title(text) except Exception: self.printError() if realTitle: return realTitle elif self.fileNames[self.ind]: fileinfo = QFileInfo(self.fileNames[self.ind]) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def autoSaveActive(self): return self.autoSaveEnabled and self.fileNames[self.ind] and \ QFileInfo(self.fileNames[self.ind]).isWritable() def modificationChanged(self, changed): if self.autoSaveActive(): changed = False self.actionSave.setEnabled(changed) self.setWindowModified(changed) def clipboardDataChanged(self): mimeData = QApplication.instance().clipboard().mimeData() if mimeData is not None: self.actionPaste.setEnabled(mimeData.hasText()) def insertChars(self, chars): tc = self.editBoxes[self.ind].textCursor() if tc.hasSelection(): selection = tc.selectedText() if selection.startswith(chars) and selection.endswith(chars): if len(selection) > 2*len(chars): selection = selection[len(chars):-len(chars)] tc.insertText(selection) else: tc.insertText(chars+tc.selectedText()+chars) else: tc.insertText(chars) def insertTag(self, ut): if not ut: return if isinstance(ut, int): ut = self.usefulTags[ut - 1] arg = ' style=""' if ut == 'span' else '' tc = self.editBoxes[self.ind].textCursor() if ut == 'img': toinsert = ('<a href="' + tc.selectedText() + '" target="_blank"><img src="' + tc.selectedText() + '"/></a>') elif ut == 'a': toinsert = ('<a href="' + tc.selectedText() + '" target="_blank">' + tc.selectedText() + '</a>') else: toinsert = '<'+ut+arg+'>'+tc.selectedText()+'</'+ut+'>' tc.insertText(toinsert) self.tagsBox.setCurrentIndex(0) def insertSymbol(self, num): if num: self.editBoxes[self.ind].insertPlainText('&'+self.usefulChars[num-1]+';') self.symbolBox.setCurrentIndex(0) def fileChanged(self, fileName): ind = self.fileNames.index(fileName) self.tabWidget.setCurrentIndex(ind) if not QFile.exists(fileName): self.editBoxes[ind].document().setModified(True) QMessageBox.warning(self, '', self.tr( 'This file has been deleted by other application.\n' 'Please make sure you save the file before exit.')) elif not self.editBoxes[ind].document().isModified(): # File was not modified in ReText, reload silently self.openFileMain() self.updatePreviewBox() else: text = self.tr( 'This document has been modified by other application.\n' 'Do you want to reload the file (this will discard all ' 'your changes)?\n') if self.autoSaveEnabled: text += self.tr( 'If you choose to not reload the file, auto save mode will ' 'be disabled for this session to prevent data loss.') messageBox = QMessageBox(QMessageBox.Warning, '', text) reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole) messageBox.addButton(QMessageBox.Cancel) messageBox.exec() if messageBox.clickedButton() is reloadButton: self.openFileMain() self.updatePreviewBox() else: self.autoSaveEnabled = False self.editBoxes[ind].document().setModified(True) if fileName not in self.fileSystemWatcher.files(): # https://sourceforge.net/p/retext/tickets/137/ self.fileSystemWatcher.addPath(fileName) def maybeSave(self, ind): if self.autoSaveActive(): self.saveFileCore(self.fileNames[self.ind]) return True if not self.editBoxes[ind].document().isModified(): return True self.tabWidget.setCurrentIndex(ind) ret = QMessageBox.warning(self, '', self.tr("The document has been modified.\nDo you want to save your changes?"), QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.saveFileMain(False) elif ret == QMessageBox.Cancel: return False return True def closeEvent(self, closeevent): for self.ind in range(self.tabWidget.count()): if not self.maybeSave(self.ind): return closeevent.ignore() if globalSettings.saveWindowGeometry and not self.isMaximized(): globalSettings.windowGeometry = self.saveGeometry() closeevent.accept() def viewHtml(self): htmlDlg = HtmlDialog(self) try: htmltext = self.getHtml(includeStyleSheet=False, includeTitle=False) except Exception: return self.printError() winTitle = self.getDocumentTitle(baseName=True) htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")") htmlDlg.textEdit.setPlainText(htmltext.rstrip()) htmlDlg.hl.rehighlight() htmlDlg.show() htmlDlg.raise_() htmlDlg.activateWindow() def openHelp(self): QDesktopServices.openUrl(QUrl('http://sourceforge.net/p/retext/home/Help and Support')) def aboutDialog(self): QMessageBox.about(self, self.aboutWindowTitle, '<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__)) +'</b></p>' + self.tr('Simple but powerful editor' ' for Markdown and reStructuredText') +'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011\u2013' '2015') +'<br><a href="http://sourceforge.net/p/retext/">'+self.tr('Website') +'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">' +self.tr('Markdown syntax') +'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">' +self.tr('reStructuredText syntax')+'</a></p>') def setDefaultMarkup(self, markup): self.defaultMarkup = markup defaultName = markups.get_available_markups()[0].name writeToSettings('defaultMarkup', markup.name, defaultName) oldind = self.ind for self.ind in range(len(self.previewBoxes)): self.docTypeChanged() self.ind = oldind
class Editor(CodeEditor, ComponentMixin): name = 'Code Editor' # This signal is emitted whenever the currently-open file changes and # autoreload is enabled. triggerRerender = pyqtSignal(bool) sigFilenameChanged = pyqtSignal(str) preferences = Parameter.create(name='Preferences', children=[{ 'name': 'Font size', 'type': 'int', 'value': 12 }, { 'name': 'Autoreload', 'type': 'bool', 'value': False }, { 'name': 'Line wrap', 'type': 'bool', 'value': False }, { 'name': 'Color scheme', 'type': 'list', 'values': ['Spyder', 'Monokai', 'Zenburn'], 'value': 'Spyder' }]) EXTENSIONS = 'py' def __init__(self, parent=None): self._watched_file = None super(Editor, self).__init__(parent) ComponentMixin.__init__(self) self.setup_editor(linenumbers=True, markers=True, edge_line=False, tab_mode=False, show_blanks=True, font=QFontDatabase.systemFont( QFontDatabase.FixedFont), language='Python', filename='') self._actions = \ {'File' : [QAction(icon('new'), 'New', self, shortcut='ctrl+N', triggered=self.new), QAction(icon('open'), 'Open', self, shortcut='ctrl+O', triggered=self.open), QAction(icon('save'), 'Save', self, shortcut='ctrl+S', triggered=self.save), QAction(icon('save_as'), 'Save as', self, shortcut='ctrl+shift+S', triggered=self.save_as), QAction(icon('autoreload'), 'Automatic reload and preview', self,triggered=self.autoreload, checkable=True, checked=False, objectName='autoreload'), ]} for a in self._actions.values(): self.addActions(a) self._fixContextMenu() self.updatePreferences() # autoreload support self._file_watcher = QFileSystemWatcher(self) # we wait for 50ms after a file change for the file to be written completely self._file_watch_timer = QTimer(self) self._file_watch_timer.setInterval(50) self._file_watch_timer.setSingleShot(True) self._file_watcher.fileChanged.connect( lambda val: self._file_watch_timer.start()) self._file_watch_timer.timeout.connect(self._file_changed) def _fixContextMenu(self): menu = self.menu menu.removeAction(self.run_cell_action) menu.removeAction(self.run_cell_and_advance_action) menu.removeAction(self.run_selection_action) menu.removeAction(self.re_run_last_cell_action) def updatePreferences(self, *args): self.set_color_scheme(self.preferences['Color scheme']) font = self.font() font.setPointSize(self.preferences['Font size']) self.set_font(font) self.findChild(QAction, 'autoreload') \ .setChecked(self.preferences['Autoreload']) self.toggle_wrap_mode(self.preferences['Line wrap']) def confirm_discard(self): if self.modified: rv = confirm( self, 'Please confirm', 'Current document is not saved - do you want to continue?') else: rv = True return rv def new(self): if not self.confirm_discard(): return self.set_text('') self.filename = '' self.reset_modified() def open(self): if not self.confirm_discard(): return curr_dir = Path(self.filename).abspath().dirname() fname = get_open_filename(self.EXTENSIONS, curr_dir) if fname != '': self.load_from_file(fname) def load_from_file(self, fname): self.set_text_from_file(fname) self.filename = fname self.reset_modified() def save(self): if self._filename != '': if self.preferences['Autoreload']: self._file_watcher.removePath(self.filename) self._file_watch_timer.stop() with open(self._filename, 'w') as f: f.write(self.toPlainText()) if self.preferences['Autoreload']: self._file_watcher.addPath(self.filename) self.triggerRerender.emit(True) self.reset_modified() else: self.save_as() def save_as(self): fname = get_save_filename(self.EXTENSIONS) if fname != '': with open(fname, 'w') as f: f.write(self.toPlainText()) self.filename = fname self.reset_modified() def _update_filewatcher(self): if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): self._file_watcher.removePath(self._watched_file) self._watched_file = None if self.preferences[ 'Autoreload'] and self.filename and self.filename != self._watched_file: self._watched_file = self._filename self._file_watcher.addPath(self.filename) @property def filename(self): return self._filename @filename.setter def filename(self, fname): self._filename = fname self._update_filewatcher() self.sigFilenameChanged.emit(fname) # callback triggered by QFileSystemWatcher def _file_changed(self): # neovim writes a file by removing it first # this causes QFileSystemWatcher to forget the file self._file_watcher.addPath(self._filename) self.set_text_from_file(self._filename) self.triggerRerender.emit(True) # Turn autoreload on/off. def autoreload(self, enabled): self.preferences['Autoreload'] = enabled self._update_filewatcher() def reset_modified(self): self.document().setModified(False) @property def modified(self): return self.document().isModified() def saveComponentState(self, store): if self.filename != '': store.setValue(self.name + '/state', self.filename) def restoreComponentState(self, store): filename = store.value(self.name + '/state', self.filename) if filename and filename != '': try: self.load_from_file(filename) except IOError: self._logger.warning(f'could not open {filename}')
class Scene: def __init__(self) -> None: super().__init__() # Call super to make multiple inheritance work. from UM.Scene.SceneNode import SceneNode self._root = SceneNode(name="Root") self._root.setCalculateBoundingBox(False) self._connectSignalsRoot() self._active_camera = None # type: Optional[Camera] self._ignore_scene_changes = False self._lock = threading.Lock() # Watching file for changes. self._file_watcher = QFileSystemWatcher() self._file_watcher.fileChanged.connect(self._onFileChanged) def _connectSignalsRoot(self) -> None: self._root.transformationChanged.connect(self.sceneChanged) self._root.childrenChanged.connect(self.sceneChanged) self._root.meshDataChanged.connect(self.sceneChanged) def _disconnectSignalsRoot(self) -> None: self._root.transformationChanged.disconnect(self.sceneChanged) self._root.childrenChanged.disconnect(self.sceneChanged) self._root.meshDataChanged.disconnect(self.sceneChanged) def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None: if self._ignore_scene_changes != ignore_scene_changes: self._ignore_scene_changes = ignore_scene_changes if self._ignore_scene_changes: self._disconnectSignalsRoot() else: self._connectSignalsRoot() ## Acquire the global scene lock. # # This will prevent any read or write actions on the scene from other threads, # assuming those threads also properly acquire the lock. Most notably, this # prevents the rendering thread from rendering the scene while it is changing. # Deprecated, use getSceneLock() instead. @deprecated("Please use the getSceneLock instead", "3.3") def acquireLock(self) -> None: self._lock.acquire() ## Release the global scene lock. # Deprecated, use getSceneLock() instead. @deprecated("Please use the getSceneLock instead", "3.3") def releaseLock(self) -> None: self._lock.release() ## Gets the global scene lock. # # Use this lock to prevent any read or write actions on the scene from other threads, # assuming those threads also properly acquire the lock. Most notably, this # prevents the rendering thread from rendering the scene while it is changing. def getSceneLock(self) -> threading.Lock: return self._lock ## Get the root node of the scene. def getRoot(self) -> "SceneNode": return self._root ## Change the root node of the scene def setRoot(self, node: "SceneNode") -> None: if self._root != node: if not self._ignore_scene_changes: self._disconnectSignalsRoot() self._root = node if not self._ignore_scene_changes: self._connectSignalsRoot() self.rootChanged.emit() rootChanged = Signal() ## Get the camera that should be used for rendering. def getActiveCamera(self) -> Optional[Camera]: return self._active_camera def getAllCameras(self) -> List[Camera]: cameras = [] for node in BreadthFirstIterator(self._root): # type: ignore if isinstance(node, Camera): cameras.append(node) return cameras ## Set the camera that should be used for rendering. # \param name The name of the camera to use. def setActiveCamera(self, name: str) -> None: camera = self.findCamera(name) if camera: self._active_camera = camera else: Logger.log( "w", "Couldn't find camera with name [%s] to activate!" % name) ## Signal that is emitted whenever something in the scene changes. # \param object The object that triggered the change. sceneChanged = Signal() ## Find an object by id. # # \param object_id The id of the object to search for, as returned by the python id() method. # # \return The object if found, or None if not. def findObject(self, object_id: int) -> Optional["SceneNode"]: for node in BreadthFirstIterator(self._root): # type: ignore if id(node) == object_id: return node return None def findCamera(self, name: str) -> Optional[Camera]: for node in BreadthFirstIterator(self._root): # type: ignore if isinstance(node, Camera) and node.getName() == name: return node return None ## Add a file to be watched for changes. # \param file_path The path to the file that must be watched. def addWatchedFile(self, file_path: str) -> None: # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive if not Platform.isWindows(): self._file_watcher.addPath(file_path) ## Remove a file so that it will no longer be watched for changes. # \param file_path The path to the file that must no longer be watched. def removeWatchedFile(self, file_path: str) -> None: # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive if not Platform.isWindows(): self._file_watcher.removePath(file_path) ## Triggered whenever a file is changed that we currently have loaded. def _onFileChanged(self, file_path: str) -> None: if not os.path.isfile(file_path) or os.path.getsize( file_path) == 0: # File doesn't exist any more, or it is empty return # Multiple nodes may be loaded from the same file at different stages. Reload them all. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator # To find which nodes to reload when files have changed. modified_nodes = [ node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path ] # type: ignore if modified_nodes: self._reload_message = Message(i18n_catalog.i18nc( "@info", "Would you like to reload {filename}?").format( filename=os.path.basename(file_path)), title=i18n_catalog.i18nc( "@info:title", "File has been modified")) self._reload_message.addAction( "reload", i18n_catalog.i18nc("@action:button", "Reload"), icon="", description=i18n_catalog.i18nc( "@action:description", "This will trigger the modified files to reload from disk." )) self._reload_callback = functools.partial(self._reloadNodes, modified_nodes) self._reload_message.actionTriggered.connect(self._reload_callback) self._reload_message.show() ## Reloads a list of nodes after the user pressed the "Reload" button. # \param nodes The list of nodes that needs to be reloaded. # \param message The message that triggered the action to reload them. # \param action The button that triggered the action to reload them. def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None: if action != "reload": return self._reload_message.hide() for node in nodes: meshdata = node.getMeshData() if meshdata: filename = meshdata.getFileName() if not filename or not os.path.isfile( filename): # File doesn't exist any more. continue job = ReadMeshJob(filename) self._reload_finished_callback = functools.partial( self._reloadJobFinished, node) job.finished.connect(self._reload_finished_callback) job.start() ## Triggered when reloading has finished. # # This then puts the resulting mesh data in the node. def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None: for node in job.getResult(): mesh_data = node.getMeshData() if mesh_data: replaced_node.setMeshData(mesh_data) else: Logger.log("w", "Could not find a mesh in reloaded node.")
class SubFolderWatcher(QObject): finished = pyqtSignal(str, int) def __init__(self): super(SubFolderWatcher, self).__init__() self.base_path = '' self.subfolder = '' self.files = {} self.finished_checking = False self.watcher = QFileSystemWatcher() self.timer = QTimer() self.check_threshold = 3 self.check_number = 0 self.sub_folder = '' def connect(self): self.watcher.addPaths([self.base_path]) self.watcher.directoryChanged.connect( lambda y: self.directory_changed(y)) self.watcher.fileChanged.connect(lambda x: self.file_changed(x)) self.timer.timeout.connect(self.check_files) self.timer.start(timer_delay * 1000) def check_files(self): finished = True if exists(self.base_path): for sub_path in Path(self.base_path).rglob('*'): new_st_mtime = int(stat(sub_path).st_mtime) path_name = normpath(sub_path) if path_name not in self.files: self.files[path_name] = new_st_mtime if self.files[path_name] != new_st_mtime: finished = False self.files[path_name] = new_st_mtime if finished: self.check_number += 1 if self.check_number > self.check_threshold: self.timer.stop() self.watcher.removePath(self.base_path) self.copy_folder() else: self.finished_checking = True def copy_file(self): pass def copy_folder(self): try: shutil.move(self.base_path, join(home, self.subfolder)) except shutil.Error: remove(self.base_path) else: pass """ NOTE: On my home system, I sometimes use the following code to change groups and owner permissions of the copied files. I would personally not use this in a production environment, especially with the 775 permissions. The following code doesn't make sense to run on Windows, so you can ignore if you're on that system. local = join(home, self.subfolder) chmod(local, 0o775) shutil.chown(local, 'grp', 'usr') for root, dirs, files in walk(local): chmod(root, 0o775) shutil.chown(root, 'grp', 'usr') for file_name in files: chmod(join(root, file_name), 0o775) shutil.chown(join(root, file_name), 'grp', 'usr') """ self.finished_checking = True self.finished.emit(self.base_path) @pyqtSlot(str) def directory_changed(self, path): if self.timer.isActive(): self.timer.stop() self.timer.start(timer_delay * 1000) for sub_path in Path(path).rglob('*'): if isfile(sub_path): path_name = normpath(join(path, sub_path.name)) self.files[path_name] = int(stat(self.base_path).st_mtime) def file_changed(self): pass def __eq__(self, other): try: return other.base_path == self.base_path except AttributeError: return other == self.base_path
class ExportDialog(QDialog): def __init__(self, font, parent=None): super().__init__(parent, Qt.MSWindowsFixedSizeDialogHint) self.setWindowModality(Qt.WindowModal) self.setWindowTitle(self.tr("Export…")) self._exportDirectory = QDir.toNativeSeparators( QStandardPaths.standardLocations(QStandardPaths.DocumentsLocation)[0] ) self.baseName = getAttrWithFallback(font.info, "postscriptFontName") self.formatBtnSet = ButtonSet(self) self.formatBtnSet.setOptions(["OTF", "TTF"]) self.formatBtnSet.setSelectionMode(ButtonSet.OneOrMoreSelection) self.compressionBtnSet = ButtonSet(self) self.compressionBtnSet.setOptions(["None", "WOFF", "WOFF2"]) self.compressionBtnSet.setSelectionMode(ButtonSet.OneOrMoreSelection) self.numberLabel = QLabel(self) self.formatBtnSet.clicked.connect(self.updateNumbers) self.compressionBtnSet.clicked.connect(self.updateNumbers) self.removeOverlapBox = QCheckBox(self.tr("Remove Overlap"), self) # self.removeOverlapBox.setChecked(True) # XXX: implement self.removeOverlapBox.setEnabled(False) self.autohintBox = QCheckBox(self.tr("Autohint"), self) # self.autohintBox.setChecked(True) # XXX: implement self.autohintBox.setEnabled(False) self.exportBox = QCheckBox(self) boxSize = self.exportBox.sizeHint() self.exportBox.setText(self.tr("Use Export Directory")) self.exportBox.setChecked(True) self.exportIcon = QLabel(self) icon = self.style().standardIcon(QStyle.SP_DirClosedIcon) iconSize = QSize(24, 24) self.exportIcon.setPixmap(icon.pixmap(icon.actualSize(iconSize))) self.exportIcon.setBaseSize(iconSize) self.exportDirLabel = QLabel(self) self.exportDirLabel.setText(self.exportDirectory) self.exportDirButton = QPushButton(self) self.exportDirButton.setText(self.tr("Choose…")) self.exportDirButton.clicked.connect( lambda: self.chooseExportDir(self.exportDirectory) ) # if files are to be overwritten, put up a warning # + use a file system watcher to avoid TOCTOU self.warningIcon = QLabel(self) icon = icons.i_warning() iconSize = QSize(20, 20) self.warningIcon.setPixmap(icon.pixmap(icon.actualSize(iconSize))) self.warningIcon.setBaseSize(iconSize) # XXX: not sure why this is needed self.warningIcon.setFixedWidth(iconSize.width()) sp = self.warningIcon.sizePolicy() sp.setRetainSizeWhenHidden(True) self.warningIcon.setSizePolicy(sp) self.warningLabel = QLabel(self) palette = self.warningLabel.palette() role, color = self.warningLabel.foregroundRole(), QColor(230, 20, 20) palette.setColor(palette.Active, role, color) palette.setColor(palette.Inactive, role, color) self.warningLabel.setPalette(palette) sp = self.warningLabel.sizePolicy() sp.setRetainSizeWhenHidden(True) self.warningLabel.setSizePolicy(sp) self.updateExportStatus() self.exportBox.toggled.connect(self.updateExportStatus) self.watcher = QFileSystemWatcher(self) self.watcher.addPath(self.exportDirectory) self.updateNumbers() self.watcher.directoryChanged.connect(self.updateNumbers) buttonBox = QDialogButtonBox(QDialogButtonBox.Cancel) buttonBox.addButton(self.tr("Generate…"), QDialogButtonBox.AcceptRole) buttonBox.accepted.connect(self.finish) buttonBox.rejected.connect(self.reject) layout = QVBoxLayout(self) formLayout = QFormLayout() formLayout.addRow(self.tr("Format"), self.formatBtnSet) formLayout.addRow(self.tr("Compression"), self.compressionBtnSet) formLayout.setHorizontalSpacing(16) formLayout.setContentsMargins(0, 0, 0, 4) layout.addLayout(formLayout) layout.addWidget(self.numberLabel) layout.addWidget(self.removeOverlapBox) layout.addWidget(self.autohintBox) layout.addWidget(self.exportBox) exportLayout = QHBoxLayout() exportLayout.addWidget(self.exportIcon) exportLayout.addWidget(self.exportDirLabel) exportLayout.addWidget(self.exportDirButton) exportLayout.addWidget(QWidget()) margins = exportLayout.contentsMargins() margins.setLeft(margins.left() + boxSize.width() + 4) exportLayout.setContentsMargins(margins) exportLayout.setStretch(3, 1) layout.addLayout(exportLayout) warningLayout = QHBoxLayout() warningLayout.addWidget(self.warningIcon) warningLayout.addWidget(self.warningLabel) warningLayout.addWidget(QWidget()) margins.setBottom(margins.bottom() + 4) warningLayout.setContentsMargins(margins) warningLayout.setStretch(3, 1) layout.addLayout(warningLayout) layout.addWidget(buttonBox) # XXX: check this on non-Windows platforms layout.setContentsMargins(16, 16, 16, 16) self.readSettings() def readSettings(self): attrs = [ (settings.exportFileFormats, self.formatBtnSet.setSelectedOptions), ( settings.exportCompressionFormats, self.compressionBtnSet.setSelectedOptions, ), (settings.exportRemoveOverlap, self.removeOverlapBox.setChecked), (settings.exportAutohint, self.autohintBox.setChecked), (settings.exportUseDirectory, self.exportBox.setChecked), ( settings.exportDirectory, lambda attr: setattr(self, "exportDirectory", attr), ), ] for getValue, setter in attrs: value = getValue() if value != "": setter(value) self.exportDirLabel.setText(self.exportDirectory) self.updateNumbers() def writeSettings(self): attrs = [ (settings.setExportFileFormats, self.formatBtnSet.selectedOptions), ( settings.setExportCompressionFormats, self.compressionBtnSet.selectedOptions, ), (settings.setExportRemoveOverlap, self.removeOverlapBox.isChecked), (settings.setExportAutohint, self.autohintBox.isChecked), (settings.setExportUseDirectory, self.exportBox.isChecked), (settings.setExportDirectory, lambda: getattr(self, "exportDirectory")), ] for setValue, getter in attrs: value = getter() setValue(value) @property def exportDirectory(self): return self._exportDirectory @exportDirectory.setter def exportDirectory(self, path): oldValue = self._exportDirectory if oldValue == path: return if oldValue is not None: self.watcher.removePath(oldValue) self._exportDirectory = path self.watcher.addPath(self.exportDirectory) self.exportDirLabel.setText(self._exportDirectory) self.updateNumbers() def chooseExportDir(self, givenDir=None): state = settings.exportFileDialogState() dialog = QFileDialog(self) if state: dialog.restoreState(state) dialogDir = dialog.directory() if givenDir is not None: dialog.setDirectory(givenDir) elif dialogDir is None: dialog.setDirectory( QStandardPaths.standardLocations(QStandardPaths.DocumentsLocation)[0] ) dialog.setAcceptMode(QFileDialog.AcceptOpen) dialog.setFileMode(QFileDialog.Directory) ok = dialog.exec_() exportDir = QDir.toNativeSeparators(dialog.directory().absolutePath()) if givenDir is not None: dialog.setDirectory(dialogDir) settings.setExportFileDialogState(dialog.saveState()) if ok: self.exportDirectory = exportDir def updateExportStatus(self): value = self.exportBox.isChecked() for w in ( self.exportIcon, self.exportDirLabel, self.exportDirButton, self.warningIcon, self.warningLabel, ): w.setEnabled(value) def updateNumbers(self): formatOptions = self.formatBtnSet.selectedOptions() compressionOptions = self.compressionBtnSet.selectedOptions() # number label count = len(formatOptions) * len(compressionOptions) self.numberLabel.setText( self.tr(f"×%n font(s) with base name: {self.baseName}*", n=count) ) # overwrite status # XXX: not DRY with the TFont.export logic count = 0 # make a list out of this, otherwise we'll consume the iterator compressions = list(map(str.lower, compressionOptions)) for format in map(str.lower, formatOptions): filePath = os.path.join(self.exportDirectory, f"{self.baseName}.{format}") for compression in compressions: fullPath = filePath if compression != "none": fullPath += f".{compression}" count += os.path.exists(fullPath) visible = bool(count) self.warningIcon.setVisible(visible) self.warningLabel.setVisible(visible) if visible: self.warningLabel.setText( self.tr("%n file(s) will be overwritten.", n=count) ) def finish(self): self.accept() export = self.exportBox.isChecked() # here we pick a directory, but it won't become the default export dir if not export: self.chooseExportDir() @classmethod def getExportParameters(cls, parent, font): dialog = cls(font, parent) result = dialog.exec_() params = dict( baseName=dialog.baseName, formats=dialog.formatBtnSet.selectedOptions(), compression=dialog.compressionBtnSet.selectedOptions(), exportDirectory=dialog.exportDirectory, removeOverlap=dialog.removeOverlapBox.isChecked(), autohint=dialog.autohintBox.isChecked(), ) return (params, result) # ---------- # Qt methods # ---------- def accept(self): self.writeSettings() super().accept() def reject(self): self.writeSettings() super().reject() def closeEvent(self, event): super().closeEvent(event) if event.isAccepted(): self.writeSettings()
class PugdebugDocuments(QObject): watcher = None open_documents = {} document_changed = pyqtSignal(object) def __init__(self): super(PugdebugDocuments, self).__init__() self.watcher = QFileSystemWatcher() self.watcher.fileChanged.connect(self.handle_file_changed) def open_document(self, path): path_key = self.get_path_key(path) document = PugdebugDocument(path) self.open_documents[path_key] = document self.watcher.addPath(path) return document def close_document(self, path): path_key = self.get_path_key(path) self.open_documents.pop(path_key, None) self.watcher.removePath(path) def refresh_document(self, path): """Refresh a document Gets called when the file system watcher notices a change to an open document. """ path_key = self.get_path_key(path) document = self.open_documents[path_key] document.read_file(path) self.document_changed.emit(document) def is_document_open(self, path): path_key = self.get_path_key(path) return path_key in self.open_documents def handle_file_changed(self, path): """Handle when a watched file gets changed Crazy stuff ahead. If a file is modified, some editors (systems?) will first remove the file and then write it back to the disk. And for that split second, the watcher will drop the file from being watched. But then again, maybe that file really got deleted? Who knows?! Anyway, when a file gets modified, we sleep a short while to see if that file will "get back" and if so, add it back to the watcher. If not, we'll assume the file got deleted. """ if not self.__is_path_watched(path): fileinfo = QFileInfo(path) total_slept = 0 file_exists = fileinfo.exists() while not file_exists: sleep_for = 0.1 total_slept += sleep_for if total_slept > 1: break time.sleep(sleep_for) file_exists = fileinfo.exists() if file_exists: self.watcher.addPath(path) self.refresh_document(path) else: # file got deleted? pass def get_path_key(self, path): path_key = hashlib.md5(path.encode("utf-8")) return path_key.hexdigest() def __is_path_watched(self, path): return path in self.watcher.files()
class AppMain(Ui_MainWindow): RAM_VIEW_INITIAL_SIZE = 10000 TEMP_MAX_RAM_USE = 1024 * 1000 STEP_TIMER_IN_MS = 100 def __init__(self): Ui_MainWindow.__init__(self) ## class Variables self.config_dialog = None self.rom_stream = None self.rom_path = None self.rom_model = None self.rom_watcher = None self.ram_model = None self.sim_real_line_old = 0 self.sim_real_line_current = 0 self.local_stack_model = None self.global_stack_model = None self.data_changed = None self.lst_parser = None self.sim_line = 0 self.last_step = None self.asm_thread = None self.sim_thread = None self.vm_thread = None self.vm_task = None self.simulator_task = None self.assembler_task = None self.config_dialog_ui = None self.step_timer = None self.window = AppMainWindow() # Setup Dialog, Editor, Actions, Threads, Img Resizing self.setup_dialog() self.setupUi(self.window) self.setup_editor() self.setup_actions() self.setup_threads() def setup_editor(self): self.rom_stream = tempfile.SpooledTemporaryFile( max_size=self.TEMP_MAX_RAM_USE, mode="w+") self.rom_path = None self.lst_parser = None self.rom_watcher = QFileSystemWatcher() self.rom_watcher.fileChanged.connect(self.reload_rom) self.spinBox.setValue(500) self.on_new() def setup_threads(self): self.asm_thread = QThread() self.sim_thread = QThread() self.vm_thread = QThread() def setup_dialog(self): self.config_dialog = QDialog() self.config_dialog_ui = config_dialog.Ui_Dialog() self.config_dialog_ui.setupUi(self.config_dialog) self.config_dialog_ui.assemblerLineEdit.setText( "../jar/Z01-Assembler.jar") self.config_dialog_ui.rtlLineEdit.setText("../Z01-Simulator-rtl-2/") def setup_clean_views(self, table, rows=100, caption="Dados", line_header=None): model = QStandardItemModel(rows, 1, self.window) model.setHorizontalHeaderItem(0, QStandardItem(caption)) table.setModel(model) for k in range(0, table.horizontalHeader().count()): table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) for l in range(0, rows): if line_header is None: model.setHeaderData(l, QtCore.Qt.Vertical, l) else: model.setHeaderData(l, QtCore.Qt.Vertical, line_header(l)) return model def setup_actions(self): self.step_timer = QtCore.QTimer() self.step_timer.timeout.connect(self.on_proximo) self.actionNovo.triggered.connect(self.on_new) self.actionSalvar_ROM.triggered.connect(self.on_save) self.actionAbrir.triggered.connect(self.on_load) self.actionProximo.triggered.connect(self.on_proximo) self.actionExecutarFim.triggered.connect(self.on_executar_fim) self.actionParar.triggered.connect(self.on_parar) self.actionEraseRAM.triggered.connect(self.on_clear_ram) self.actionVoltarInicio.triggered.connect(self.on_voltar_inicio) self.spinBox.valueChanged.connect(self.on_voltar_inicio) self.config_dialog_ui.procurarButton.clicked.connect( self.on_search_assembler) self.config_dialog_ui.alterarButton.clicked.connect( self.config_dialog.close) self.actionConfiguracoes.triggered.connect(self.config_dialog.show) def change_rtl_dir(self, new_dir): self.config_dialog_ui.rtlLineEdit.setText(new_dir) def on_ram_tooltip(self, item): text = item.text().strip() try: val = int(text, 2) except ValueError: return item.setToolTip("{0:d} dec - {1:x} hex".format(val, val)) def on_clear_ram(self): self.ram_model = self.setup_clean_views( self.ramView, rows=self.RAM_VIEW_INITIAL_SIZE, caption="RAM", line_header=asm_utils.z01_ram_name) for i in range(0, self.RAM_VIEW_INITIAL_SIZE): item = QStandardItem("0000000000000000") self.on_ram_tooltip(item) self.ram_model.setItem(i, item) def on_new(self): self.rom_path = None self.on_clear_ram() self.rom_model = self.setup_clean_views(self.romView, caption="Program") #self.local_stack_model = self.setup_clean_views(self.localStackView, caption="Local Stack") self.global_stack_model = self.setup_clean_views( self.globalStackView, caption="Global Stack", line_header=vm_utils.vm_global_stack_name) self.rom_model.itemChanged.connect(self.valid_rom) self.ram_model.itemChanged.connect(self.valid_ram) self.actionROMAssembly.setEnabled(True) self.clear_simulation() def on_voltar_inicio(self): self.data_changed = True self.clear_simulation() def on_parar(self): self.step_timer.stop() def on_executar_fim(self): self.step_timer.start(self.STEP_TIMER_IN_MS) def show(self): self.window.show() def on_save(self): filename = self.rom_path if self.rom_path is not None: self.rom_watcher.removePath(self.rom_path) if filename is None: filename = QFileDialog.getSaveFileName(self.window, "Salve o arquivo", os.getcwd(), "Arquivos (*.vm)") if len(filename) == 0 or len(filename[0]) == 0: return None filename = filename[0] self.rom_path = filename file_utils.copy_model_to_file(self.rom_model, self.rom_stream) file_utils.stream_to_file(self.rom_stream, filename) self.rom_watcher.addPath(self.rom_path) def on_load(self): filename = QFileDialog.getOpenFileName(self.window, "Escolha arquivo", os.getcwd() + "/examples/", "Arquivos (*.vm)") if len(filename) == 0 or len(filename[0]) == 0: return None if self.rom_path is not None: self.rom_watcher.removePath(self.rom_path) self.on_new() self.rom_path = filename[0] self.rom_watcher.addPath(self.rom_path) self.reload_rom() def reload_rom(self): return self.load_rom(self.rom_path) def load_rom(self, filename): if not os.path.exists(filename): return if filename.endswith(".asm") or filename.endswith(".vm"): self.load_vm(filename, self.rom_model) def on_search_assembler(self): filename = QFileDialog.getOpenFileName(self.window, "Escolha arquivo", os.getcwd(), "Arquivo JAR (*.jar)") if len(filename) == 0 or len(filename[0]) == 0: return None self.config_dialog_ui.assemblerLineEdit.setText(filename[0]) def on_proximo(self): if self.data_changed: if self.lst_parser is not None: self.lst_parser.close() self.sim_line = 0 file_utils.copy_model_to_file(self.rom_model, self.rom_stream) self.vm_translate(self.vm_end) return False while (True): #time.sleep(0.4) # esse timer nao funcionou como eu esperava ! self.sim_real_line_old = self.sim_real_line_current step = self.lst_parser.advance() if "s_regAout" not in step: self.step_timer.stop() QMessageBox.warning(self.window, "Simulador", "Fim de simulação") return False self.update_line_edit(self.lineEdit_SP, self.model_get_value(self.ram_model, 0)) self.update_line_edit(self.lineEdit_LCL, self.model_get_value(self.ram_model, 1)) self.update_line_edit(self.lineEdit_ARG, self.model_get_value(self.ram_model, 2)) self.update_line_edit(self.lineEdit_THIS, self.model_get_value(self.ram_model, 3)) self.update_line_edit(self.lineEdit_THAT, self.model_get_value(self.ram_model, 4)) sp_index = self.ram_model.index(0, 0) # SP if self.last_step is not None: addr = int(step["s_regAout"], 2) index = self.ram_model.index(addr, 0) last_sp = int(self.ram_model.itemFromIndex(sp_index).text(), 2) if int(step["writeM"]) == 0 and int( step["s_muxALUI_A"]) == 1 and int( self.last_step["s_muxALUI_A"]) == 0: self.ramView.setCurrentIndex(index) if int(step["writeM"]) == 1: self.ramView.setCurrentIndex(index) self.ram_model.itemFromIndex(index).setText(step["outM"]) else: last_sp = 256 ## descobrir linha com base no SP sp = int(self.ram_model.itemFromIndex(sp_index).text(), 2) if sp != last_sp: self.sim_line += 1 self.refresh_stack(self.ram_model, 256, sp, self.global_stack_model, self.globalStackView) ## update line pc = int(step["pcout"], 2) - 1 self.sim_real_line_current = vm_utils.vm_command_line( self.assembler_task.commands_pos, self.assembler_task.comments_pos, self.assembler_task.labels_pos, pc) index = self.rom_model.index(self.sim_real_line_current, 0) self.romView.setCurrentIndex(index) print("PROXIMA INSTRUCAO NASM") self.last_step = step if (self.sim_real_line_current != self.sim_real_line_old): print("PROXIMA INSTRUCAO VM") break def model_get_value(self, model, row): index = model.index(row, 0) return model.itemFromIndex(index).text() def model_set_value(self, model, row, value, tooltip=False): index = model.index(row, 0) model.itemFromIndex(index).setText(value) self.on_ram_tooltip(model.itemFromIndex(index)) if tooltip: self.on_ram_tooltip(model.itemFromIndex(index)) def refresh_stack(self, ram_model, start_addr, end_addr, target_model, target_view): line = 0 for i in range(start_addr, end_addr, 1): self.model_set_value(target_model, line, self.model_get_value(ram_model, i)) line += 1 index = target_model.index(line, 0) target_view.setCurrentIndex(index) for i in range(line, line + 10): self.model_set_value(target_model, i, "") def update_line_edit(self, line_edit, new_value, ignore=False): if line_edit.text() != new_value: line_edit.setText(new_value) if not ignore: line_edit.setStyleSheet( "QLineEdit {background-color: yellow;}") self.on_ram_tooltip(line_edit) else: line_edit.setStyleSheet("") def valid_rom(self, item): if not item.text(): return None index = item.index() while index.row() + 50 >= self.rom_model.rowCount(): self.rom_model.appendRow(QStandardItem("")) if not vm_utils.vm_valid_command(item.text()): item.setText("") def valid_binary(self, item): valid = True text = item.text().strip() try: val = int(text, 2) except ValueError: valid = False if not valid: print("Invalid BIN Instruction: {}".format(item.text())) return valid def valid_ram(self, item): if not item.text(): return None text = item.text() index = item.index() while index.row() + 100 >= self.ram_model.rowCount(): self.ram_model.appendRow(QStandardItem("{0:0>16b}".format(0))) if text.startswith("d"): text = text[1:] if text.isdigit(): item.setText("{0:0>16b}".format(int(text))) valid = self.valid_binary(item) if valid: self.on_ram_tooltip(item) else: item.setText("{0:0>16b}".format(0)) def vm_translate(self, callback): if self.asm_thread.isRunning() or self.sim_thread.isRunning( ) or self.vm_thread.isRunning(): print( "[vm_translate] Tarefas de simulação em processamento por favor aguarde finalizar...." ) return False vm_translator = "java -jar ../jar/Z01-VMTranslator.jar" self.vm_task = VMTask(vm_translator, "temp/", True) nasm_out = tempfile.SpooledTemporaryFile( max_size=self.TEMP_MAX_RAM_USE, mode="w+") self.vm_task.setup(self.rom_stream, nasm_out) self.vm_task.finished.connect(callback) self.vm_task.moveToThread(self.vm_thread) self.vm_thread.started.connect(self.vm_task.run) self.vm_thread.start() def assemble(self, callback, nasm_file): if self.asm_thread.isRunning() or self.sim_thread.isRunning(): print( "[assemble] Tarefas de simulação em processamento por favor aguarde finalizar...." ) return False assembler = "java -jar " + self.config_dialog_ui.assemblerLineEdit.text( ) self.assembler_task = AssemblerTask(assembler, "temp/") rom_out = tempfile.SpooledTemporaryFile(max_size=self.TEMP_MAX_RAM_USE, mode="w+") self.assembler_task.setup(nasm_file, rom_out) self.assembler_task.finished.connect(callback) self.assembler_task.moveToThread(self.asm_thread) self.asm_thread.started.connect(self.assembler_task.run) self.asm_thread.start() def simulate(self, rom_file, ram_file): if self.asm_thread.isRunning() or self.sim_thread.isRunning(): print( "[simulate] Tarefas de simulação em processamento por favor aguarde finalizar...." ) return False self.simulator_task = SimulatorTask( "temp/", False, self.config_dialog_ui.simGUIBox.isChecked(), self.config_dialog_ui.rtlLineEdit.text()) lst_out = tempfile.SpooledTemporaryFile(max_size=self.TEMP_MAX_RAM_USE, mode="w+") self.simulator_task.setup(rom_file, ram_file, lst_out, self.spinBox.value() * 10 + 10) self.simulator_task.finished.connect(self.simulation_end) self.simulator_task.moveToThread(self.sim_thread) self.sim_thread.started.connect(self.simulator_task.run) self.sim_thread.start() self.lock_and_show_dialog() def lock_and_show_dialog(self): ## waits for ASM thread and SIM thread to end self.progress_dialog = QProgressDialog("Simulando...", "Cancelar", 0, 0, self.window) self.progress_dialog.setCancelButton(None) self.progress_dialog.setAutoReset(True) self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) self.progress_dialog.setMinimumDuration(0) self.progress_dialog.setValue(0) self.progress_dialog.setWindowTitle("RESimulatorGUI") self.progress_dialog.setWindowFlags(self.progress_dialog.windowFlags() & ~QtCore.Qt.WindowCloseButtonHint) while self.asm_thread.isRunning() or self.sim_thread.isRunning(): qapp.processEvents() self.progress_dialog.reset() def get_updated_ram(self): ram = tempfile.SpooledTemporaryFile(max_size=self.TEMP_MAX_RAM_USE, mode="w+") file_utils.copy_model_to_file(self.ram_model, ram) return ram def check_assembler_sucess(self): if self.assembler_task is not None and self.assembler_task.success is True: return True QMessageBox.critical(self.window, "Assembler", "Erro ao traduzir assembly.") self.step_timer.stop() return False def check_vm_sucess(self): if self.vm_task is not None and self.vm_task.success is True: return True QMessageBox.critical(self.window, "VM Translator", "Erro ao traduzir código VM para Assembly.") self.step_timer.stop() return False def vm_end(self): self.vm_thread.quit() self.vm_thread.wait() if not self.check_vm_sucess(): return print("VM Translator done!") self.assemble(self.assemble_end, self.vm_task.stream_out) def assemble_end(self): self.asm_thread.quit() # ensure end of thread self.asm_thread.wait() ram = self.get_updated_ram() if not self.check_assembler_sucess(): return print("ASM done!") self.simulate(self.assembler_task.stream_out, ram) def simulation_end(self): self.sim_thread.quit() #ensure end of thread self.sim_thread.wait() print("SIM done!") self.data_changed = False self.lst_parser = LSTParser(self.simulator_task.lst_stream) def clear_simulation(self): self.last_step = None self.update_line_edit(self.lineEdit_SP, "0000000000000000", True) self.update_line_edit(self.lineEdit_LCL, "0000000000000000", True) self.update_line_edit(self.lineEdit_ARG, "0000000000000000", True) self.update_line_edit(self.lineEdit_THIS, "0000000000000000", True) self.update_line_edit(self.lineEdit_THAT, "0000000000000000", True) self.data_changed = True index = self.ram_model.index(0, 0) self.ramView.setCurrentIndex(index) index = self.rom_model.index(0, 0) self.romView.setCurrentIndex(index) def load_file(self, filename, model): fp = open(filename, "r") counter = 0 lines = file_utils.file_len(filename) self.rom_model = self.setup_clean_views(self.romView, rows=lines + 200, caption="Program") for i, l in enumerate(fp): if vm_utils.vm_valid_command(l.strip()): index = self.rom_model.index(counter, 0) self.rom_model.itemFromIndex(index).setText(l.strip()) counter += 1 fp.close() def load_vm(self, filename, model): self.load_file(filename, model)
class Scene: def __init__(self) -> None: super().__init__() from UM.Scene.SceneNode import SceneNode self._root = SceneNode(name = "Root") self._root.setCalculateBoundingBox(False) self._connectSignalsRoot() self._active_camera = None # type: Optional[Camera] self._ignore_scene_changes = False self._lock = threading.Lock() # Watching file for changes. self._file_watcher = QFileSystemWatcher() self._file_watcher.fileChanged.connect(self._onFileChanged) self._reload_message = None # type: Optional[Message] def _connectSignalsRoot(self) -> None: self._root.transformationChanged.connect(self.sceneChanged) self._root.childrenChanged.connect(self.sceneChanged) self._root.meshDataChanged.connect(self.sceneChanged) def _disconnectSignalsRoot(self) -> None: self._root.transformationChanged.disconnect(self.sceneChanged) self._root.childrenChanged.disconnect(self.sceneChanged) self._root.meshDataChanged.disconnect(self.sceneChanged) def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None: if self._ignore_scene_changes != ignore_scene_changes: self._ignore_scene_changes = ignore_scene_changes if self._ignore_scene_changes: self._disconnectSignalsRoot() else: self._connectSignalsRoot() ## Gets the global scene lock. # # Use this lock to prevent any read or write actions on the scene from other threads, # assuming those threads also properly acquire the lock. Most notably, this # prevents the rendering thread from rendering the scene while it is changing. def getSceneLock(self) -> threading.Lock: return self._lock ## Get the root node of the scene. def getRoot(self) -> "SceneNode": return self._root ## Change the root node of the scene def setRoot(self, node: "SceneNode") -> None: if self._root != node: if not self._ignore_scene_changes: self._disconnectSignalsRoot() self._root = node if not self._ignore_scene_changes: self._connectSignalsRoot() self.rootChanged.emit() rootChanged = Signal() ## Get the camera that should be used for rendering. def getActiveCamera(self) -> Optional[Camera]: return self._active_camera def getAllCameras(self) -> List[Camera]: cameras = [] for node in BreadthFirstIterator(self._root): # type: ignore if isinstance(node, Camera): cameras.append(node) return cameras ## Set the camera that should be used for rendering. # \param name The name of the camera to use. def setActiveCamera(self, name: str) -> None: camera = self.findCamera(name) if camera: self._active_camera = camera else: Logger.log("w", "Couldn't find camera with name [%s] to activate!" % name) ## Signal that is emitted whenever something in the scene changes. # \param object The object that triggered the change. sceneChanged = Signal() ## Find an object by id. # # \param object_id The id of the object to search for, as returned by the python id() method. # # \return The object if found, or None if not. def findObject(self, object_id: int) -> Optional["SceneNode"]: for node in BreadthFirstIterator(self._root): # type: ignore if id(node) == object_id: return node return None def findCamera(self, name: str) -> Optional[Camera]: for node in BreadthFirstIterator(self._root): # type: ignore if isinstance(node, Camera) and node.getName() == name: return node return None ## Add a file to be watched for changes. # \param file_path The path to the file that must be watched. def addWatchedFile(self, file_path: str) -> None: # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive if not Platform.isWindows(): self._file_watcher.addPath(file_path) ## Remove a file so that it will no longer be watched for changes. # \param file_path The path to the file that must no longer be watched. def removeWatchedFile(self, file_path: str) -> None: # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive if not Platform.isWindows(): self._file_watcher.removePath(file_path) ## Triggered whenever a file is changed that we currently have loaded. def _onFileChanged(self, file_path: str) -> None: if not os.path.isfile(file_path) or os.path.getsize(file_path) == 0: # File doesn't exist any more, or it is empty return # Multiple nodes may be loaded from the same file at different stages. Reload them all. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator # To find which nodes to reload when files have changed. modified_nodes = [node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path] # type: ignore if modified_nodes: # Hide the message if it was already visible if self._reload_message is not None: self._reload_message.hide() self._reload_message = Message(i18n_catalog.i18nc("@info", "Would you like to reload {filename}?").format(filename = os.path.basename(file_path)), title = i18n_catalog.i18nc("@info:title", "File has been modified")) self._reload_message.addAction("reload", i18n_catalog.i18nc("@action:button", "Reload"), icon = "", description = i18n_catalog.i18nc("@action:description", "This will trigger the modified files to reload from disk.")) self._reload_callback = functools.partial(self._reloadNodes, modified_nodes) self._reload_message.actionTriggered.connect(self._reload_callback) self._reload_message.show() ## Reloads a list of nodes after the user pressed the "Reload" button. # \param nodes The list of nodes that needs to be reloaded. # \param message The message that triggered the action to reload them. # \param action The button that triggered the action to reload them. def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None: if action != "reload": return if self._reload_message is not None: self._reload_message.hide() for node in nodes: meshdata = node.getMeshData() if meshdata: filename = meshdata.getFileName() if not filename or not os.path.isfile(filename): # File doesn't exist any more. continue job = ReadMeshJob(filename) self._reload_finished_callback = functools.partial(self._reloadJobFinished, node) job.finished.connect(self._reload_finished_callback) job.start() ## Triggered when reloading has finished. # # This then puts the resulting mesh data in the node. def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None: for node in job.getResult(): mesh_data = node.getMeshData() if mesh_data: replaced_node.setMeshData(mesh_data) else: Logger.log("w", "Could not find a mesh in reloaded node.")
class EditorWorker(QObject): file_change_sig = pyqtSignal(bool) # is_zim def __init__(self, command, old_text, is_zim, zim_folder=None, docid=None, zim_file=None, parent=None): '''Observe the write/save states of a txt file to edit notes Args: command (list): command string list to pass into Popen. old_text (str): existing note text to paste into editor. is_zim (bool): if True, try open the associated zim note file. if False, open a temp file to edit. Kwargs: zim_folder (str or None): if not None, the path to the zim note folder, used to search for zim notes. docid (int or None): if not None, id of current doc. parent (QWidget or None): parent widget. ''' super(EditorWorker, self).__init__(parent) self.is_zim = is_zim self.zim_folder = zim_folder self.docid = docid self.logger = logging.getLogger(__name__) if not self.is_zim: self._temp_file = QTemporaryFile(self) else: if zim_file is not None: # use given zim file if os.path.exists(zim_file) and os.path.islink(zim_file): self._temp_file = QFile(zim_file, self) self.logger.debug('Got given zim file %s' % zim_file) else: try: zim_file = locateZimNote(self.zim_folder, self.docid) self._temp_file = QFile(zim_file, self) self.logger.exception( 'Failed to open given zim file. Get the id one.') except: self.logger.exception('Failed to find zim file.') self._temp_file = QTemporaryFile(self) self.is_zim = False else: # no given zim file, get the one in all_notes try: zim_file = locateZimNote(self.zim_folder, self.docid) self._temp_file = QFile(zim_file, self) self.logger.debug('Got zim file %s' % zim_file) except: self.logger.exception('Failed to find zim file.') self._temp_file = QTemporaryFile(self) self.is_zim = False self._process = QProcess(self) self._text = "" self._watcher = QFileSystemWatcher(self) self._watcher.fileChanged.connect(self.onFileChange) # write existing lines if temp file if not self.is_zim and self._temp_file.open(): self._temp_file.write(old_text.encode('utf-8')) self._temp_file.close() # open() on temp file assumes QIODevice.ReadWrite as well. if self._temp_file.open(QIODevice.ReadWrite): self._file_path = self._temp_file.fileName() self._watcher.addPath(self._file_path) self.logger.debug('_file_path = %s' % self._file_path) program = command[0] arguments = command[1:] self._process.start(program, arguments + [self._temp_file.fileName()]) @pyqtSlot() def onFileChange(self): if self._temp_file.isOpen(): #self._temp_file.seek(0) #self._text = self._temp_file.readAll().data().decode() # has to use with open and read(), the above doesn't work for # some editors, like xed # For some reason, if watching the zim file, and open in gvim # it reports file not found unless I wait for a while. wtf = os.path.exists(self._temp_file.fileName()) while not wtf: wtf = os.path.exists(self._temp_file.fileName()) with open(self._temp_file.fileName()) as tmp: self._text = tmp.read() # Re-add watch file, again for xed. self._watcher.removePath(self._file_path) self._watcher.addPath(self._file_path) self.file_change_sig.emit(self.is_zim) @property def text(self): return self._text def __del__(self): self._process.kill()
class Scene: """Container object for the scene graph The main purpose of this class is to provide the root SceneNode. """ def __init__(self) -> None: super().__init__() from UM.Scene.SceneNode import SceneNode self._root = SceneNode(name = "Root") self._root.setCalculateBoundingBox(False) self._connectSignalsRoot() self._active_camera = None # type: Optional[Camera] self._ignore_scene_changes = False self._lock = threading.Lock() # Watching file for changes. self._file_watcher = QFileSystemWatcher() self._file_watcher.fileChanged.connect(self._onFileChanged) self._reload_message = None # type: Optional[Message] self._callbacks = set() # type: Set[Callable] # Need to keep these in memory. This is a memory leak every time you refresh, but a tiny one. def _connectSignalsRoot(self) -> None: self._root.transformationChanged.connect(self.sceneChanged) self._root.childrenChanged.connect(self.sceneChanged) self._root.meshDataChanged.connect(self.sceneChanged) def _disconnectSignalsRoot(self) -> None: self._root.transformationChanged.disconnect(self.sceneChanged) self._root.childrenChanged.disconnect(self.sceneChanged) self._root.meshDataChanged.disconnect(self.sceneChanged) def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None: if self._ignore_scene_changes != ignore_scene_changes: self._ignore_scene_changes = ignore_scene_changes if self._ignore_scene_changes: self._disconnectSignalsRoot() else: self._connectSignalsRoot() @deprecated("Scene lock is no longer used", "4.5") def getSceneLock(self) -> threading.Lock: return self._lock def getRoot(self) -> "SceneNode": """Get the root node of the scene.""" return self._root def setRoot(self, node: "SceneNode") -> None: """Change the root node of the scene""" if self._root != node: if not self._ignore_scene_changes: self._disconnectSignalsRoot() self._root = node if not self._ignore_scene_changes: self._connectSignalsRoot() self.rootChanged.emit() rootChanged = Signal() def getActiveCamera(self) -> Optional[Camera]: """Get the camera that should be used for rendering.""" return self._active_camera def getAllCameras(self) -> List[Camera]: cameras = [] for node in BreadthFirstIterator(self._root): if isinstance(node, Camera): cameras.append(node) return cameras def setActiveCamera(self, name: str) -> None: """Set the camera that should be used for rendering. :param name: The name of the camera to use. """ camera = self.findCamera(name) if camera and camera != self._active_camera: if self._active_camera: self._active_camera.perspectiveChanged.disconnect(self.sceneChanged) self._active_camera = camera self._active_camera.perspectiveChanged.connect(self.sceneChanged) else: Logger.log("w", "Couldn't find camera with name [%s] to activate!" % name) sceneChanged = Signal() """Signal that is emitted whenever something in the scene changes. :param object: The object that triggered the change. """ def findObject(self, object_id: int) -> Optional["SceneNode"]: """Find an object by id. :param object_id: The id of the object to search for, as returned by the python id() method. :return: The object if found, or None if not. """ for node in BreadthFirstIterator(self._root): if id(node) == object_id: return node return None def findCamera(self, name: str) -> Optional[Camera]: for node in BreadthFirstIterator(self._root): if isinstance(node, Camera) and node.getName() == name: return node return None def addWatchedFile(self, file_path: str) -> None: """Add a file to be watched for changes. :param file_path: The path to the file that must be watched. """ # File watcher causes cura to crash on windows if threaded from removable device (usb, ...). Create QEventLoop earlier to fix this. if Platform.isWindows(): QEventLoop() self._file_watcher.addPath(file_path) def removeWatchedFile(self, file_path: str) -> None: """Remove a file so that it will no longer be watched for changes. :param file_path: The path to the file that must no longer be watched. """ self._file_watcher.removePath(file_path) def _onFileChanged(self, file_path: str) -> None: """Triggered whenever a file is changed that we currently have loaded.""" try: if os.path.getsize(file_path) == 0: # File is empty. return except EnvironmentError: # Or it doesn't exist any more, or we have no access any more. return # Multiple nodes may be loaded from the same file at different stages. Reload them all. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator # To find which nodes to reload when files have changed. modified_nodes = [node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path] # type: ignore if modified_nodes: # Hide the message if it was already visible if self._reload_message is not None: self._reload_message.hide() self._reload_message = Message(i18n_catalog.i18nc("@info", "Would you like to reload {filename}?").format(filename = os.path.basename(file_path)), title = i18n_catalog.i18nc("@info:title", "File has been modified")) self._reload_message.addAction("reload", i18n_catalog.i18nc("@action:button", "Reload"), icon = "", description = i18n_catalog.i18nc("@action:description", "This will trigger the modified files to reload from disk.")) self._reload_callback = functools.partial(self._reloadNodes, modified_nodes) self._reload_message.actionTriggered.connect(self._reload_callback) self._reload_message.show() def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None: """Reloads a list of nodes after the user pressed the "Reload" button. :param nodes: The list of nodes that needs to be reloaded. :param message: The message that triggered the action to reload them. :param action: The button that triggered the action to reload them. """ if action != "reload": return if self._reload_message is not None: self._reload_message.hide() for node in nodes: meshdata = node.getMeshData() if meshdata: filename = meshdata.getFileName() if not filename or not os.path.isfile(filename): # File doesn't exist any more. continue job = ReadMeshJob(filename) reload_finished_callback = functools.partial(self._reloadJobFinished, node) self._callbacks.add(reload_finished_callback) #Store it so it won't get garbage collected. This is a memory leak, but just one partial per reload so it's not much. job.finished.connect(reload_finished_callback) job.start() def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None: """Triggered when reloading has finished. This then puts the resulting mesh data in the node. """ for node in job.getResult(): mesh_data = node.getMeshData() if mesh_data: replaced_node.setMeshData(mesh_data) else: Logger.log("w", "Could not find a mesh in reloaded node.")
class Editor(CodeEditor,ComponentMixin): name = 'Code Editor' # This signal is emitted whenever the currently-open file changes and # autoreload is enabled. triggerRerender = pyqtSignal(bool) sigFilenameChanged = pyqtSignal(str) preferences = Parameter.create(name='Preferences',children=[ {'name': 'Font size', 'type': 'int', 'value': 12}, {'name': 'Autoreload', 'type': 'bool', 'value': False}, {'name': 'Color scheme', 'type': 'list', 'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}]) EXTENSIONS = '*.py' def __init__(self,parent=None): super(Editor,self).__init__(parent) ComponentMixin.__init__(self) self._filename = '' self.setup_editor(linenumbers=True, markers=True, edge_line=False, tab_mode=False, show_blanks=True, language='Python') self._actions = \ {'File' : [QAction(icon('new'), 'New', self, shortcut='ctrl+N', triggered=self.new), QAction(icon('open'), 'Open', self, shortcut='ctrl+O', triggered=self.open), QAction(icon('save'), 'Save', self, shortcut='ctrl+S', triggered=self.save), QAction(icon('save_as'), 'Save as', self, shortcut='ctrl+shift+S', triggered=self.save_as), QAction(icon('autoreload'), 'Automatic reload and preview', self,triggered=self.autoreload, checkable=True, checked=False, objectName='autoreload'), ]} for a in self._actions.values(): self.addActions(a) self._fixContextMenu() self.updatePreferences() # autoreload support self._file_watcher = QFileSystemWatcher(self) self._watched_file = None self._file_watcher.fileChanged.connect(self._file_changed) def _fixContextMenu(self): menu = self.menu menu.removeAction(self.run_cell_action) menu.removeAction(self.run_cell_and_advance_action) menu.removeAction(self.run_selection_action) menu.removeAction(self.re_run_last_cell_action) def updatePreferences(self,*args): self.set_color_scheme(self.preferences['Color scheme']) font = self.font() font.setPointSize(self.preferences['Font size']) self.set_font(font) self.findChild(QAction, 'autoreload') \ .setChecked(self.preferences['Autoreload']) def new(self): self.set_text('') self.filename = '' self.reset_modified() def open(self): fname,_ = QFileDialog.getOpenFileName(self,filter=self.EXTENSIONS) if fname is not '': self.load_from_file(fname) def load_from_file(self,fname): self.set_text_from_file(fname) self.filename = fname self.reset_modified() def save(self): if self._filename is not '': if self.preferences['Autoreload']: self._file_watcher.removePath(self.filename) with open(self._filename,'w') as f: f.write(self.toPlainText()) if self.preferences['Autoreload']: self._file_watcher.addPath(self.filename) self.triggerRerender.emit(True) self.reset_modified() else: self.save_as() def save_as(self): fname,_ = QFileDialog.getSaveFileName(self,filter=self.EXTENSIONS) if fname is not '': with open(fname,'w') as f: f.write(self.toPlainText()) self.filename = fname self.reset_modified() def _update_filewatcher(self): if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): self._file_watcher.removePath(self._watched_file) self._watched_file = None if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file: self._watched_file = self._filename self._file_watcher.addPath(self.filename) @property def filename(self): return self._filename @filename.setter def filename(self, fname): self._filename = fname self._update_filewatcher() self.sigFilenameChanged.emit(fname) # callback triggered by QFileSystemWatcher def _file_changed(self, val): self.set_text_from_file(self._filename) self.triggerRerender.emit(True) # Turn autoreload on/off. def autoreload(self, enabled): self.preferences['Autoreload'] = enabled self._update_filewatcher() def reset_modified(self): self.document().setModified(False) def saveComponenetState(self,store): if self.filename is not '': store.setValue(self.name+'/state',self.filename) def restoreComponenetState(self,store): filename = store.value(self.name+'/state',self.filename) if filename and filename is not '': self.load_from_file(filename)
class ReTextWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.resize(950, 700) screenRect = QDesktopWidget().screenGeometry() if globalSettings.windowGeometry: self.restoreGeometry(globalSettings.windowGeometry) else: self.move((screenRect.width()-self.width())/2, (screenRect.height()-self.height())/2) if not screenRect.contains(self.geometry()): self.showMaximized() if globalSettings.iconTheme: QIcon.setThemeName(globalSettings.iconTheme) if QIcon.themeName() in ('hicolor', ''): if not QFile.exists(icon_path + 'document-new.png'): QIcon.setThemeName(get_icon_theme()) if QFile.exists(icon_path+'retext.png'): self.setWindowIcon(QIcon(icon_path+'retext.png')) elif QFile.exists('/usr/share/pixmaps/retext.png'): self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png')) else: self.setWindowIcon(QIcon.fromTheme('retext', QIcon.fromTheme('accessories-text-editor'))) self.editBoxes = [] self.previewBoxes = [] self.highlighters = [] self.markups = [] self.fileNames = [] self.actionPreviewChecked = [] self.actionLivePreviewChecked = [] self.tabWidget = QTabWidget(self) self.initTabWidget() self.setCentralWidget(self.tabWidget) self.tabWidget.currentChanged.connect(self.changeIndex) self.tabWidget.tabCloseRequested.connect(self.closeTab) toolBar = QToolBar(self.tr('File toolbar'), self) self.addToolBar(Qt.TopToolBarArea, toolBar) self.editBar = QToolBar(self.tr('Edit toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.editBar) self.searchBar = QToolBar(self.tr('Search toolbar'), self) self.addToolBar(Qt.BottomToolBarArea, self.searchBar) toolBar.setVisible(not globalSettings.hideToolBar) self.editBar.setVisible(not globalSettings.hideToolBar) self.actionNew = self.act(self.tr('New'), 'document-new', self.createNew, shct=QKeySequence.New) self.actionNew.setPriority(QAction.LowPriority) self.actionOpen = self.act(self.tr('Open'), 'document-open', self.openFile, shct=QKeySequence.Open) self.actionOpen.setPriority(QAction.LowPriority) self.actionSetEncoding = self.act(self.tr('Set encoding'), trig=self.showEncodingDialog) self.actionSetEncoding.setEnabled(False) self.actionReload = self.act(self.tr('Reload'), 'view-refresh', trig=self.openFileMain) self.actionReload.setEnabled(False) self.actionSave = self.act(self.tr('Save'), 'document-save', self.saveFile, shct=QKeySequence.Save) self.actionSave.setEnabled(False) self.actionSave.setPriority(QAction.LowPriority) self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as', self.saveFileAs, shct=QKeySequence.SaveAs) self.actionNextTab = self.act(self.tr('Next tab'), 'go-next', lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown) self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous', lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp) self.actionPrint = self.act(self.tr('Print'), 'document-print', self.printFile, shct=QKeySequence.Print) self.actionPrint.setPriority(QAction.LowPriority) self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview', self.printPreview) self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml) self.actionChangeEditorFont = self.act(self.tr('Change editor font'), trig=self.changeEditorFont) self.actionChangePreviewFont = self.act(self.tr('Change preview font'), trig=self.changePreviewFont) self.actionSearch = self.act(self.tr('Find text'), 'edit-find', shct=QKeySequence.Find) self.actionSearch.setCheckable(True) self.actionSearch.triggered[bool].connect(self.searchBar.setVisible) self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged) self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E, trigbool=self.preview) if QIcon.hasThemeIcon('document-preview'): self.actionPreview.setIcon(QIcon.fromTheme('document-preview')) elif QIcon.hasThemeIcon('preview-file'): self.actionPreview.setIcon(QIcon.fromTheme('preview-file')) elif QIcon.hasThemeIcon('x-office-document'): self.actionPreview.setIcon(QIcon.fromTheme('x-office-document')) else: self.actionPreview.setIcon(QIcon(icon_path+'document-preview.png')) self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L, trigbool=self.enableLivePreview) self.actionTableMode = self.act(self.tr('Table mode'), shct=Qt.CTRL+Qt.Key_T, trigbool=lambda x: self.editBoxes[self.ind].enableTableMode(x)) if ReTextFakeVimHandler: self.actionFakeVimMode = self.act(self.tr('FakeVim mode'), shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode) if globalSettings.useFakeVim: self.actionFakeVimMode.setChecked(True) self.enableFakeVimMode(True) self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen', shct=Qt.Key_F11, trigbool=self.enableFullScreen) self.actionFullScreen.setPriority(QAction.LowPriority) self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system', trig=self.openConfigDialog) self.actionConfig.setMenuRole(QAction.PreferencesRole) self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml) self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf) self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf) self.getExportExtensionsList() self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit) self.actionQuit.setMenuRole(QAction.QuitRole) self.actionQuit.triggered.connect(self.close) self.actionUndo = self.act(self.tr('Undo'), 'edit-undo', lambda: self.editBoxes[self.ind].undo(), shct=QKeySequence.Undo) self.actionRedo = self.act(self.tr('Redo'), 'edit-redo', lambda: self.editBoxes[self.ind].redo(), shct=QKeySequence.Redo) self.actionCopy = self.act(self.tr('Copy'), 'edit-copy', lambda: self.editBoxes[self.ind].copy(), shct=QKeySequence.Copy) self.actionCut = self.act(self.tr('Cut'), 'edit-cut', lambda: self.editBoxes[self.ind].cut(), shct=QKeySequence.Cut) self.actionPaste = self.act(self.tr('Paste'), 'edit-paste', lambda: self.editBoxes[self.ind].paste(), shct=QKeySequence.Paste) self.actionUndo.setEnabled(False) self.actionRedo.setEnabled(False) self.actionCopy.setEnabled(False) self.actionCut.setEnabled(False) qApp = QApplication.instance() qApp.clipboard().dataChanged.connect(self.clipboardDataChanged) self.clipboardDataChanged() if enchant_available: self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck) self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale) self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit) self.actionWebKit.setChecked(globalSettings.useWebKit) self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir) self.actionFind = self.act(self.tr('Next'), 'go-next', self.find, shct=QKeySequence.FindNext) self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous', lambda: self.find(back=True), shct=QKeySequence.FindPrevious) self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp) self.aboutWindowTitle = self.tr('About ReText') self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog) self.actionAbout.setMenuRole(QAction.AboutRole) self.actionAboutQt = self.act(self.tr('About Qt')) self.actionAboutQt.setMenuRole(QAction.AboutQtRole) self.actionAboutQt.triggered.connect(qApp.aboutQt) availableMarkups = markups.get_available_markups() if not availableMarkups: print('Warning: no markups are available!') self.defaultMarkup = availableMarkups[0] if availableMarkups else None if globalSettings.defaultMarkup: mc = markups.find_markup_class_by_name(globalSettings.defaultMarkup) if mc and mc.available(): self.defaultMarkup = mc if len(availableMarkups) > 1: self.chooseGroup = QActionGroup(self) markupActions = [] for markup in availableMarkups: markupAction = self.act(markup.name, trigbool=self.markupFunction(markup)) if markup == self.defaultMarkup: markupAction.setChecked(True) self.chooseGroup.addAction(markupAction) markupActions.append(markupAction) self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold, trig=lambda: self.insertChars('**')) self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic, trig=lambda: self.insertChars('*')) self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline, trig=lambda: self.insertTag('u')) self.usefulTags = ('a', 'big', 'center', 'img', 's', 'small', 'span', 'table', 'td', 'tr', 'u') self.usefulChars = ('deg', 'divide', 'dollar', 'hellip', 'laquo', 'larr', 'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo', 'rarr', 'rsquo', 'times') self.tagsBox = QComboBox(self.editBar) self.tagsBox.addItem(self.tr('Tags')) self.tagsBox.addItems(self.usefulTags) self.tagsBox.activated.connect(self.insertTag) self.symbolBox = QComboBox(self.editBar) self.symbolBox.addItem(self.tr('Symbols')) self.symbolBox.addItems(self.usefulChars) self.symbolBox.activated.connect(self.insertSymbol) self.updateStyleSheet() menubar = QMenuBar(self) menubar.setGeometry(QRect(0, 0, 800, 25)) self.setMenuBar(menubar) menuFile = menubar.addMenu(self.tr('File')) menuEdit = menubar.addMenu(self.tr('Edit')) menuHelp = menubar.addMenu(self.tr('Help')) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent')) self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles) menuFile.addMenu(self.menuRecentFiles) menuFile.addAction(self.actionShow) menuFile.addAction(self.actionSetEncoding) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addSeparator() menuFile.addAction(self.actionNextTab) menuFile.addAction(self.actionPrevTab) menuFile.addSeparator() menuExport = menuFile.addMenu(self.tr('Export')) menuExport.addAction(self.actionSaveHtml) menuExport.addAction(self.actionOdf) menuExport.addAction(self.actionPdf) if self.extensionActions: menuExport.addSeparator() for action, mimetype in self.extensionActions: menuExport.addAction(action) menuExport.aboutToShow.connect(self.updateExtensionsVisibility) menuFile.addAction(self.actionPrint) menuFile.addAction(self.actionPrintPreview) menuFile.addSeparator() menuFile.addAction(self.actionQuit) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addSeparator() if enchant_available: menuSC = menuEdit.addMenu(self.tr('Spell check')) menuSC.addAction(self.actionEnableSC) menuSC.addAction(self.actionSetLocale) menuEdit.addAction(self.actionSearch) menuEdit.addAction(self.actionChangeEditorFont) menuEdit.addAction(self.actionChangePreviewFont) menuEdit.addSeparator() if len(availableMarkups) > 1: self.menuMode = menuEdit.addMenu(self.tr('Default markup')) for markupAction in markupActions: self.menuMode.addAction(markupAction) menuFormat = menuEdit.addMenu(self.tr('Formatting')) menuFormat.addAction(self.actionBold) menuFormat.addAction(self.actionItalic) menuFormat.addAction(self.actionUnderline) menuEdit.addAction(self.actionWebKit) menuEdit.addSeparator() menuEdit.addAction(self.actionViewHtml) menuEdit.addAction(self.actionLivePreview) menuEdit.addAction(self.actionPreview) menuEdit.addAction(self.actionTableMode) if ReTextFakeVimHandler: menuEdit.addAction(self.actionFakeVimMode) menuEdit.addSeparator() menuEdit.addAction(self.actionFullScreen) menuEdit.addAction(self.actionConfig) menuHelp.addAction(self.actionHelp) menuHelp.addSeparator() menuHelp.addAction(self.actionAbout) menuHelp.addAction(self.actionAboutQt) menubar.addMenu(menuFile) menubar.addMenu(menuEdit) menubar.addMenu(menuHelp) toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) toolBar.addAction(self.actionNew) toolBar.addSeparator() toolBar.addAction(self.actionOpen) toolBar.addAction(self.actionSave) toolBar.addAction(self.actionPrint) toolBar.addSeparator() toolBar.addAction(self.actionPreview) toolBar.addAction(self.actionFullScreen) self.editBar.addAction(self.actionUndo) self.editBar.addAction(self.actionRedo) self.editBar.addSeparator() self.editBar.addAction(self.actionCut) self.editBar.addAction(self.actionCopy) self.editBar.addAction(self.actionPaste) self.editBar.addSeparator() self.editBar.addWidget(self.tagsBox) self.editBar.addWidget(self.symbolBox) self.searchEdit = QLineEdit(self.searchBar) self.searchEdit.setPlaceholderText(self.tr('Search')) self.searchEdit.returnPressed.connect(self.find) self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar) self.searchBar.addWidget(self.searchEdit) self.searchBar.addSeparator() self.searchBar.addWidget(self.csBox) self.searchBar.addAction(self.actionFindPrev) self.searchBar.addAction(self.actionFind) self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.searchBar.setVisible(False) self.autoSaveEnabled = globalSettings.autoSave if self.autoSaveEnabled: timer = QTimer(self) timer.start(60000) timer.timeout.connect(self.saveAll) self.ind = None if enchant_available: self.sl = globalSettings.spellCheckLocale if self.sl: try: enchant.Dict(self.sl) except Exception as e: print(e, file=sys.stderr) self.sl = None if globalSettings.spellCheck: self.actionEnableSC.setChecked(True) self.enableSpellCheck(True) self.fileSystemWatcher = QFileSystemWatcher() self.fileSystemWatcher.fileChanged.connect(self.fileChanged) def updateStyleSheet(self): if globalSettings.styleSheet: sheetfile = QFile(globalSettings.styleSheet) sheetfile.open(QIODevice.ReadOnly) self.ss = QTextStream(sheetfile).readAll() sheetfile.close() else: self.ss = '' def initTabWidget(self): def dragEnterEvent(e): e.acceptProposedAction() def dropEvent(e): fn = bytes(e.mimeData().data('text/plain')).decode().rstrip() if fn.startswith('file:'): fn = QUrl(fn).toLocalFile() self.openFileWrapper(fn) self.tabWidget.setTabsClosable(True) self.tabWidget.setAcceptDrops(True) self.tabWidget.dragEnterEvent = dragEnterEvent self.tabWidget.dropEvent = dropEvent def act(self, name, icon=None, trig=None, trigbool=None, shct=None): if not isinstance(shct, QKeySequence): shct = QKeySequence(shct) if icon: action = QAction(self.actIcon(icon), name, self) else: action = QAction(name, self) if trig: action.triggered.connect(trig) elif trigbool: action.setCheckable(True) action.triggered[bool].connect(trigbool) if shct: action.setShortcut(shct) return action def actIcon(self, name): return QIcon.fromTheme(name, QIcon(icon_path+name+'.png')) def printError(self): import traceback print('Exception occured while parsing document:', file=sys.stderr) traceback.print_exc() def getSplitter(self, index): splitter = QSplitter(Qt.Horizontal) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBoxes[index], self.previewBoxes[index]: widget.setMinimumWidth(125) splitter.addWidget(widget) splitter.setSizes((50, 50)) splitter.setChildrenCollapsible(False) return splitter def getWebView(self): webView = QWebView() if not globalSettings.handleWebLinks: webView.page().setLinkDelegationPolicy(QWebPage.DelegateExternalLinks) webView.page().linkClicked.connect(QDesktopServices.openUrl) settings = webView.settings() settings.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, False) settings.setDefaultTextEncoding('utf-8') return webView def createTab(self, fileName): self.previewBlocked = False self.editBoxes.append(ReTextEdit(self)) self.highlighters.append(ReTextHighlighter(self.editBoxes[-1].document())) if enchant_available and self.actionEnableSC.isChecked(): self.highlighters[-1].dictionary = \ enchant.Dict(self.sl) if self.sl else enchant.Dict() self.highlighters[-1].rehighlight() if globalSettings.useWebKit: self.previewBoxes.append(self.getWebView()) else: self.previewBoxes.append(QTextBrowser()) self.previewBoxes[-1].setOpenExternalLinks(True) self.previewBoxes[-1].setVisible(False) self.fileNames.append(fileName) markupClass = self.getMarkupClass(fileName) self.markups.append(self.getMarkup(fileName)) self.highlighters[-1].docType = (markupClass.name if markupClass else '') liveMode = globalSettings.restorePreviewState and globalSettings.previewState self.actionPreviewChecked.append(liveMode) self.actionLivePreviewChecked.append(liveMode) metrics = QFontMetrics(self.editBoxes[-1].font()) self.editBoxes[-1].setTabStopWidth(globalSettings.tabWidth * metrics.width(' ')) self.editBoxes[-1].textChanged.connect(self.updateLivePreviewBox) self.editBoxes[-1].undoAvailable.connect(self.actionUndo.setEnabled) self.editBoxes[-1].redoAvailable.connect(self.actionRedo.setEnabled) self.editBoxes[-1].copyAvailable.connect(self.enableCopy) self.editBoxes[-1].document().modificationChanged.connect(self.modificationChanged) if globalSettings.useFakeVim: self.installFakeVimHandler(self.editBoxes[-1]) return self.getSplitter(-1) def closeTab(self, ind): if self.maybeSave(ind): if self.tabWidget.count() == 1: self.tabWidget.addTab(self.createTab(""), self.tr("New document")) if self.fileNames[ind]: self.fileSystemWatcher.removePath(self.fileNames[ind]) del self.editBoxes[ind] del self.previewBoxes[ind] del self.highlighters[ind] del self.markups[ind] del self.fileNames[ind] del self.actionPreviewChecked[ind] del self.actionLivePreviewChecked[ind] self.tabWidget.removeTab(ind) def getMarkupClass(self, fileName=None): if fileName is None: fileName = self.fileNames[self.ind] if fileName: markupClass = markups.get_markup_for_file_name( fileName, return_class=True) if markupClass: return markupClass return self.defaultMarkup def getMarkup(self, fileName=None): if fileName is None: fileName = self.fileNames[self.ind] markupClass = self.getMarkupClass(fileName=fileName) if markupClass and markupClass.available(): return markupClass(filename=fileName) def docTypeChanged(self): oldType = self.highlighters[self.ind].docType markupClass = self.getMarkupClass() newType = markupClass.name if markupClass else '' if oldType != newType: self.markups[self.ind] = self.getMarkup() self.updatePreviewBox() self.highlighters[self.ind].docType = newType self.highlighters[self.ind].rehighlight() dtMarkdown = (newType == DOCTYPE_MARKDOWN) dtMkdOrReST = (newType in (DOCTYPE_MARKDOWN, DOCTYPE_REST)) self.tagsBox.setEnabled(dtMarkdown) self.symbolBox.setEnabled(dtMarkdown) self.actionUnderline.setEnabled(dtMarkdown) self.actionBold.setEnabled(dtMkdOrReST) self.actionItalic.setEnabled(dtMkdOrReST) canReload = bool(self.fileNames[self.ind]) and not self.autoSaveActive() self.actionSetEncoding.setEnabled(canReload) self.actionReload.setEnabled(canReload) def changeIndex(self, ind): if ind > -1: self.actionUndo.setEnabled(self.editBoxes[ind].document().isUndoAvailable()) self.actionRedo.setEnabled(self.editBoxes[ind].document().isRedoAvailable()) self.actionCopy.setEnabled(self.editBoxes[ind].textCursor().hasSelection()) self.actionCut.setEnabled(self.editBoxes[ind].textCursor().hasSelection()) self.actionPreview.setChecked(self.actionPreviewChecked[ind]) self.actionLivePreview.setChecked(self.actionLivePreviewChecked[ind]) self.actionTableMode.setChecked(self.editBoxes[ind].tableModeEnabled) self.editBar.setDisabled(self.actionPreviewChecked[ind]) self.ind = ind if self.fileNames[ind]: self.setCurrentFile() else: self.setWindowTitle(self.tr('New document') + '[*]') self.docTypeChanged() self.modificationChanged(self.editBoxes[ind].document().isModified()) if globalSettings.restorePreviewState: globalSettings.previewState = self.actionLivePreviewChecked[ind] if self.actionLivePreviewChecked[ind]: self.enableLivePreview(True) self.editBoxes[self.ind].setFocus(Qt.OtherFocusReason) def changeEditorFont(self): font, ok = QFontDialog.getFont(globalSettings.editorFont, self) if ok: globalSettings.editorFont = font for editor in self.editBoxes: editor.updateFont() def changePreviewFont(self): font, ok = QFontDialog.getFont(globalSettings.font, self) if ok: globalSettings.font = font self.updatePreviewBox() def preview(self, viewmode): self.actionPreviewChecked[self.ind] = viewmode if self.actionLivePreview.isChecked(): self.actionLivePreview.setChecked(False) return self.enableLivePreview(False) self.editBar.setDisabled(viewmode) self.editBoxes[self.ind].setVisible(not viewmode) self.previewBoxes[self.ind].setVisible(viewmode) if viewmode: self.updatePreviewBox() def enableLivePreview(self, livemode): if globalSettings.restorePreviewState: globalSettings.previewState = livemode self.actionLivePreviewChecked[self.ind] = livemode self.actionPreviewChecked[self.ind] = livemode self.actionPreview.setChecked(livemode) self.editBar.setEnabled(True) self.previewBoxes[self.ind].setVisible(livemode) self.editBoxes[self.ind].setVisible(True) if livemode: self.updatePreviewBox() def enableWebKit(self, enable): globalSettings.useWebKit = enable oldind = self.ind self.tabWidget.clear() for self.ind in range(len(self.editBoxes)): if enable: self.previewBoxes[self.ind] = self.getWebView() else: self.previewBoxes[self.ind] = QTextBrowser() self.previewBoxes[self.ind].setOpenExternalLinks(True) splitter = self.getSplitter(self.ind) self.tabWidget.addTab(splitter, self.getDocumentTitle(baseName=True)) self.updatePreviewBox() self.previewBoxes[self.ind].setVisible(self.actionPreviewChecked[self.ind]) self.ind = oldind self.tabWidget.setCurrentIndex(self.ind) def enableCopy(self, copymode): self.actionCopy.setEnabled(copymode) self.actionCut.setEnabled(copymode) def enableFullScreen(self, yes): if yes: self.showFullScreen() else: self.showNormal() def openConfigDialog(self): dlg = ConfigDialog(self) dlg.setWindowTitle(self.tr('Preferences')) dlg.show() def installFakeVimHandler(self, editor): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(editor, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) self.actionFakeVimMode.triggered.connect(fakeVimEditor.remove) def enableFakeVimMode(self, yes): globalSettings.useFakeVim = yes if yes: FakeVimMode.init(self) for editor in self.editBoxes: self.installFakeVimHandler(editor) else: FakeVimMode.exit(self) def enableSpellCheck(self, yes): if yes: if self.sl: self.setAllDictionaries(enchant.Dict(self.sl)) else: self.setAllDictionaries(enchant.Dict()) else: self.setAllDictionaries(None) globalSettings.spellCheck = yes def setAllDictionaries(self, dictionary): for hl in self.highlighters: hl.dictionary = dictionary hl.rehighlight() def changeLocale(self): if self.sl: localedlg = LocaleDialog(self, defaultText=self.sl) else: localedlg = LocaleDialog(self) if localedlg.exec() != QDialog.Accepted: return sl = localedlg.localeEdit.text() setdefault = localedlg.checkBox.isChecked() if sl: try: sl = str(sl) enchant.Dict(sl) except Exception as e: QMessageBox.warning(self, '', str(e)) else: self.sl = sl self.enableSpellCheck(self.actionEnableSC.isChecked()) else: self.sl = None self.enableSpellCheck(self.actionEnableSC.isChecked()) if setdefault: globalSettings.spellCheckLocale = sl def searchBarVisibilityChanged(self, visible): self.actionSearch.setChecked(visible) if visible: self.searchEdit.setFocus(Qt.ShortcutFocusReason) def find(self, back=False): flags = QTextDocument.FindFlags() if back: flags |= QTextDocument.FindBackward if self.csBox.isChecked(): flags |= QTextDocument.FindCaseSensitively text = self.searchEdit.text() editBox = self.editBoxes[self.ind] cursor = editBox.textCursor() newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) cursor.movePosition(QTextCursor.End if back else QTextCursor.Start) newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) self.setSearchEditColor(False) def setSearchEditColor(self, found): palette = self.searchEdit.palette() palette.setColor(QPalette.Active, QPalette.Base, Qt.white if found else QColor(255, 102, 102)) self.searchEdit.setPalette(palette) def getHtml(self, includeStyleSheet=True, includeTitle=True, includeMeta=False, webenv=False): if self.markups[self.ind] is None: markupClass = self.getMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '') errMsg = errMsg.replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg text = self.editBoxes[self.ind].toPlainText() headers = '' if includeStyleSheet: headers += '<style type="text/css">\n' + self.ss + '</style>\n' cssFileName = self.getDocumentTitle(baseName=True)+'.css' if QFile(cssFileName).exists(): headers += '<link rel="stylesheet" type="text/css" href="%s">\n' \ % cssFileName if includeMeta: headers += ('<meta name="generator" content="ReText %s">\n' % app_version) fallbackTitle = self.getDocumentTitle() if includeTitle else '' return self.markups[self.ind].get_whole_html(text, custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=fallbackTitle, webenv=webenv) def updatePreviewBox(self): self.previewBlocked = False pb = self.previewBoxes[self.ind] textedit = isinstance(pb, QTextEdit) if textedit: scrollbar = pb.verticalScrollBar() disttobottom = scrollbar.maximum() - scrollbar.value() else: frame = pb.page().mainFrame() scrollpos = frame.scrollPosition() try: html = self.getHtml() except Exception: return self.printError() if textedit: pb.setHtml(html) pb.document().setDefaultFont(globalSettings.font) scrollbar.setValue(scrollbar.maximum() - disttobottom) else: pb.settings().setFontFamily(QWebSettings.StandardFont, globalSettings.font.family()) pb.settings().setFontSize(QWebSettings.DefaultFontSize, globalSettings.font.pointSize()) pb.setHtml(html, QUrl.fromLocalFile(self.fileNames[self.ind])) frame.setScrollPosition(scrollpos) def updateLivePreviewBox(self): if self.actionLivePreview.isChecked() and self.previewBlocked == False: self.previewBlocked = True QTimer.singleShot(1000, self.updatePreviewBox) def showInDir(self): if self.fileNames[self.ind]: path = QFileInfo(self.fileNames[self.ind]).path() QDesktopServices.openUrl(QUrl.fromLocalFile(path)) else: QMessageBox.warning(self, '', self.tr("Please, save the file somewhere.")) def setCurrentFile(self): self.setWindowTitle("") self.tabWidget.setTabText(self.ind, self.getDocumentTitle(baseName=True)) self.setWindowFilePath(self.fileNames[self.ind]) files = readListFromSettings("recentFileList") while self.fileNames[self.ind] in files: files.remove(self.fileNames[self.ind]) files.insert(0, self.fileNames[self.ind]) if len(files) > 10: del files[10:] writeListToSettings("recentFileList", files) QDir.setCurrent(QFileInfo(self.fileNames[self.ind]).dir().path()) self.docTypeChanged() def createNew(self, text=None): self.tabWidget.addTab(self.createTab(""), self.tr("New document")) self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if text: self.editBoxes[self.ind].textCursor().insertText(text) def switchTab(self, shift=1): self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count()) def updateRecentFiles(self): self.menuRecentFiles.clear() self.recentFilesActions = [] filesOld = readListFromSettings("recentFileList") files = [] for f in filesOld: if QFile.exists(f): files.append(f) self.recentFilesActions.append(self.act(f, trig=self.openFunction(f))) writeListToSettings("recentFileList", files) for action in self.recentFilesActions: self.menuRecentFiles.addAction(action) def markupFunction(self, markup): return lambda: self.setDefaultMarkup(markup) def openFunction(self, fileName): return lambda: self.openFileWrapper(fileName) def extensionFuntion(self, data): return lambda: \ self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension']) def getExportExtensionsList(self): extensions = [] for extsprefix in datadirs: extsdir = QDir(extsprefix+'/export-extensions/') if extsdir.exists(): for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], QDir.Files | QDir.Readable): extensions.append(self.readExtension(fileInfo.filePath())) locale = QLocale.system().name() self.extensionActions = [] for extension in extensions: try: if ('Name[%s]' % locale) in extension: name = extension['Name[%s]' % locale] elif ('Name[%s]' % locale.split('_')[0]) in extension: name = extension['Name[%s]' % locale.split('_')[0]] else: name = extension['Name'] data = {} for prop in ('FileFilter', 'DefaultExtension', 'Exec'): if 'X-ReText-'+prop in extension: data[prop] = extension['X-ReText-'+prop] elif prop in extension: data[prop] = extension[prop] else: data[prop] = '' action = self.act(name, trig=self.extensionFuntion(data)) if 'Icon' in extension: action.setIcon(self.actIcon(extension['Icon'])) mimetype = extension['MimeType'] if 'MimeType' in extension else None except KeyError: print('Failed to parse extension: Name is required', file=sys.stderr) else: self.extensionActions.append((action, mimetype)) def updateExtensionsVisibility(self): markupClass = self.getMarkupClass() for action in self.extensionActions: if markupClass is None: action[0].setEnabled(False) continue mimetype = action[1] if mimetype == None: enabled = True elif markupClass == markups.MarkdownMarkup: enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown")) elif markupClass == markups.ReStructuredTextMarkup: enabled = (mimetype in ("text/x-retext-rst", "text/x-rst")) else: enabled = False action[0].setEnabled(enabled) def readExtension(self, fileName): extFile = QFile(fileName) extFile.open(QIODevice.ReadOnly) extension = {} stream = QTextStream(extFile) while not stream.atEnd(): line = stream.readLine() if '=' in line: index = line.index('=') extension[line[:index].rstrip()] = line[index+1:].lstrip() extFile.close() return extension def openFile(self): supportedExtensions = ['.txt'] for markup in markups.get_all_markups(): supportedExtensions += markup.file_extensions fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;' fileNames = QFileDialog.getOpenFileNames(self, self.tr("Select one or several files to open"), "", self.tr("Supported files") + fileFilter + self.tr("All files (*)")) for fileName in fileNames[0]: self.openFileWrapper(fileName) def openFileWrapper(self, fileName): if not fileName: return fileName = QFileInfo(fileName).canonicalFilePath() exists = False for i in range(self.tabWidget.count()): if self.fileNames[i] == fileName: exists = True ex = i if exists: self.tabWidget.setCurrentIndex(ex) elif QFile.exists(fileName): noEmptyTab = ( (self.ind is None) or self.fileNames[self.ind] or self.editBoxes[self.ind].toPlainText() or self.editBoxes[self.ind].document().isModified() ) if noEmptyTab: self.tabWidget.addTab(self.createTab(fileName), "") self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if fileName: self.fileSystemWatcher.addPath(fileName) self.fileNames[self.ind] = fileName self.openFileMain() def openFileMain(self, encoding=None): openfile = QFile(self.fileNames[self.ind]) openfile.open(QIODevice.ReadOnly) stream = QTextStream(openfile) if encoding: stream.setCodec(encoding) elif globalSettings.defaultCodec: stream.setCodec(globalSettings.defaultCodec) text = stream.readAll() openfile.close() markupClass = markups.get_markup_for_file_name( self.fileNames[self.ind], return_class=True) self.highlighters[self.ind].docType = (markupClass.name if markupClass else '') self.markups[self.ind] = self.getMarkup() if self.defaultMarkup: self.highlighters[self.ind].docType = self.defaultMarkup.name editBox = self.editBoxes[self.ind] modified = bool(encoding) and (editBox.toPlainText() != text) editBox.setPlainText(text) self.setCurrentFile() editBox.document().setModified(modified) self.setWindowModified(modified) def showEncodingDialog(self): if not self.maybeSave(self.ind): return encoding, ok = QInputDialog.getItem(self, '', self.tr('Select file encoding from the list:'), [bytes(b).decode() for b in QTextCodec.availableCodecs()], 0, False) if ok: self.openFileMain(encoding) def saveFile(self): self.saveFileMain(dlg=False) def saveFileAs(self): self.saveFileMain(dlg=True) def saveAll(self): oldind = self.ind for self.ind in range(self.tabWidget.count()): if self.fileNames[self.ind] and QFileInfo(self.fileNames[self.ind]).isWritable(): self.saveFileCore(self.fileNames[self.ind]) self.editBoxes[self.ind].document().setModified(False) self.ind = oldind def saveFileMain(self, dlg): if (not self.fileNames[self.ind]) or dlg: markupClass = self.getMarkupClass() if (markupClass is None) or not hasattr(markupClass, 'default_extension'): defaultExt = self.tr("Plain text (*.txt)") ext = ".txt" else: defaultExt = self.tr('%s files', 'Example of final string: Markdown files') \ % markupClass.name + ' (' + str.join(' ', ('*'+extension for extension in markupClass.file_extensions)) + ')' if markupClass == markups.MarkdownMarkup: ext = globalSettings.markdownDefaultFileExtension elif markupClass == markups.ReStructuredTextMarkup: ext = globalSettings.restDefaultFileExtension else: ext = markupClass.default_extension newFileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", defaultExt)[0] if newFileName: if not QFileInfo(newFileName).suffix(): newFileName += ext if self.fileNames[self.ind]: self.fileSystemWatcher.removePath(self.fileNames[self.ind]) self.fileNames[self.ind] = newFileName self.actionSetEncoding.setDisabled(self.autoSaveActive()) if self.fileNames[self.ind]: result = self.saveFileCore(self.fileNames[self.ind]) if result: self.setCurrentFile() self.editBoxes[self.ind].document().setModified(False) self.setWindowModified(False) return True else: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return False def saveFileCore(self, fn, addToWatcher=True): self.fileSystemWatcher.removePath(fn) savefile = QFile(fn) result = savefile.open(QIODevice.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBoxes[self.ind].toPlainText() savefile.close() if result and addToWatcher: self.fileSystemWatcher.addPath(fn) return result def saveHtml(self, fileName): if not QFileInfo(fileName).suffix(): fileName += ".html" try: htmltext = self.getHtml(includeStyleSheet=False, includeMeta=True, webenv=True) except Exception: return self.printError() htmlFile = QFile(fileName) htmlFile.open(QIODevice.WriteOnly) html = QTextStream(htmlFile) if globalSettings.defaultCodec: html.setCodec(globalSettings.defaultCodec) html << htmltext htmlFile.close() def textDocument(self): td = QTextDocument() td.setMetaInformation(QTextDocument.DocumentTitle, self.getDocumentTitle()) if self.ss: td.setDefaultStyleSheet(self.ss) td.setHtml(self.getHtml()) td.setDefaultFont(globalSettings.font) return td def saveOdf(self): try: document = self.textDocument() except Exception: return self.printError() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to ODT"), "", self.tr("OpenDocument text files (*.odt)"))[0] if not QFileInfo(fileName).suffix(): fileName += ".odt" writer = QTextDocumentWriter(fileName) writer.setFormat("odf") writer.write(document) def saveFileHtml(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", self.tr("HTML files (*.html *.htm)"))[0] if fileName: self.saveHtml(fileName) def getDocumentForPrint(self): if globalSettings.useWebKit: return self.previewBoxes[self.ind] try: return self.textDocument() except Exception: self.printError() def standardPrinter(self): printer = QPrinter(QPrinter.HighResolution) printer.setDocName(self.getDocumentTitle()) printer.setCreator('ReText %s' % app_version) return printer def savePdf(self): self.updatePreviewBox() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to PDF"), "", self.tr("PDF files (*.pdf)"))[0] if fileName: if not QFileInfo(fileName).suffix(): fileName += ".pdf" printer = self.standardPrinter() printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fileName) document = self.getDocumentForPrint() if document != None: document.print(printer) def printFile(self): self.updatePreviewBox() printer = self.standardPrinter() dlg = QPrintDialog(printer, self) dlg.setWindowTitle(self.tr("Print document")) if (dlg.exec() == QDialog.Accepted): document = self.getDocumentForPrint() if document != None: document.print(printer) def printPreview(self): document = self.getDocumentForPrint() if document == None: return printer = self.standardPrinter() preview = QPrintPreviewDialog(printer, self) preview.paintRequested.connect(document.print) preview.exec() def runExtensionCommand(self, command, filefilter, defaultext): of = ('%of' in command) html = ('%html' in command) if of: if defaultext and not filefilter: filefilter = '*'+defaultext fileName = QFileDialog.getSaveFileName(self, self.tr('Export document'), '', filefilter)[0] if not fileName: return if defaultext and not QFileInfo(fileName).suffix(): fileName += defaultext basename = '.%s.retext-temp' % self.getDocumentTitle(baseName=True) if html: tmpname = basename+'.html' self.saveHtml(tmpname) else: tmpname = basename+self.getMarkupClass().default_extension self.saveFileCore(tmpname, addToWatcher=False) command = command.replace('%of', '"out'+defaultext+'"') command = command.replace('%html' if html else '%if', '"'+tmpname+'"') try: Popen(str(command), shell=True).wait() except Exception as error: errorstr = str(error) QMessageBox.warning(self, '', self.tr('Failed to execute the command:') + '\n' + errorstr) QFile(tmpname).remove() if of: QFile('out'+defaultext).rename(fileName) def getDocumentTitle(self, baseName=False): markup = self.markups[self.ind] realTitle = '' if markup and not baseName: text = self.editBoxes[self.ind].toPlainText() try: realTitle = markup.get_document_title(text) except Exception: self.printError() if realTitle: return realTitle elif self.fileNames[self.ind]: fileinfo = QFileInfo(self.fileNames[self.ind]) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def autoSaveActive(self): return self.autoSaveEnabled and self.fileNames[self.ind] and \ QFileInfo(self.fileNames[self.ind]).isWritable() def modificationChanged(self, changed): if self.autoSaveActive(): changed = False self.actionSave.setEnabled(changed) self.setWindowModified(changed) def clipboardDataChanged(self): mimeData = QApplication.instance().clipboard().mimeData() if mimeData is not None: self.actionPaste.setEnabled(mimeData.hasText()) def insertChars(self, chars): tc = self.editBoxes[self.ind].textCursor() if tc.hasSelection(): selection = tc.selectedText() if selection.startswith(chars) and selection.endswith(chars): if len(selection) > 2*len(chars): selection = selection[len(chars):-len(chars)] tc.insertText(selection) else: tc.insertText(chars+tc.selectedText()+chars) else: tc.insertText(chars) def insertTag(self, ut): if not ut: return if isinstance(ut, int): ut = self.usefulTags[ut - 1] arg = ' style=""' if ut == 'span' else '' tc = self.editBoxes[self.ind].textCursor() if ut == 'img': toinsert = ('<a href="' + tc.selectedText() + '" target="_blank"><img src="' + tc.selectedText() + '"/></a>') elif ut == 'a': toinsert = ('<a href="' + tc.selectedText() + '" target="_blank">' + tc.selectedText() + '</a>') else: toinsert = '<'+ut+arg+'>'+tc.selectedText()+'</'+ut+'>' tc.insertText(toinsert) self.tagsBox.setCurrentIndex(0) def insertSymbol(self, num): if num: self.editBoxes[self.ind].insertPlainText('&'+self.usefulChars[num-1]+';') self.symbolBox.setCurrentIndex(0) def fileChanged(self, fileName): ind = self.fileNames.index(fileName) self.tabWidget.setCurrentIndex(ind) if not QFile.exists(fileName): self.editBoxes[ind].document().setModified(True) QMessageBox.warning(self, '', self.tr( 'This file has been deleted by other application.\n' 'Please make sure you save the file before exit.')) elif not self.editBoxes[ind].document().isModified(): # File was not modified in ReText, reload silently self.openFileMain() self.updatePreviewBox() else: text = self.tr( 'This document has been modified by other application.\n' 'Do you want to reload the file (this will discard all ' 'your changes)?\n') if self.autoSaveEnabled: text += self.tr( 'If you choose to not reload the file, auto save mode will ' 'be disabled for this session to prevent data loss.') messageBox = QMessageBox(QMessageBox.Warning, '', text) reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole) messageBox.addButton(QMessageBox.Cancel) messageBox.exec() if messageBox.clickedButton() is reloadButton: self.openFileMain() self.updatePreviewBox() else: self.autoSaveEnabled = False self.editBoxes[ind].document().setModified(True) if fileName not in self.fileSystemWatcher.files(): # https://github.com/retext-project/retext/issues/137 self.fileSystemWatcher.addPath(fileName) def maybeSave(self, ind): if self.autoSaveActive(): self.saveFileCore(self.fileNames[self.ind]) return True if not self.editBoxes[ind].document().isModified(): return True self.tabWidget.setCurrentIndex(ind) ret = QMessageBox.warning(self, '', self.tr("The document has been modified.\nDo you want to save your changes?"), QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.saveFileMain(False) elif ret == QMessageBox.Cancel: return False return True def closeEvent(self, closeevent): for self.ind in range(self.tabWidget.count()): if not self.maybeSave(self.ind): return closeevent.ignore() if globalSettings.saveWindowGeometry and not self.isMaximized(): globalSettings.windowGeometry = self.saveGeometry() closeevent.accept() def viewHtml(self): htmlDlg = HtmlDialog(self) try: htmltext = self.getHtml(includeStyleSheet=False, includeTitle=False) except Exception: return self.printError() winTitle = self.getDocumentTitle(baseName=True) htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")") htmlDlg.textEdit.setPlainText(htmltext.rstrip()) htmlDlg.hl.rehighlight() htmlDlg.show() htmlDlg.raise_() htmlDlg.activateWindow() def openHelp(self): QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki')) def aboutDialog(self): QMessageBox.about(self, self.aboutWindowTitle, '<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__)) +'</b></p>' + self.tr('Simple but powerful editor' ' for Markdown and reStructuredText') +'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011\u2013' '2015') +'<br><a href="https://github.com/retext-project/retext">'+self.tr('Website') +'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">' +self.tr('Markdown syntax') +'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">' +self.tr('reStructuredText syntax')+'</a></p>') def setDefaultMarkup(self, markup): self.defaultMarkup = markup defaultName = markups.get_available_markups()[0].name writeToSettings('defaultMarkup', markup.name, defaultName) oldind = self.ind for self.ind in range(len(self.previewBoxes)): self.docTypeChanged() self.ind = oldind
class NFile(QObject): """ SIGNALS: @askForSaveFileClosing(QString) @fileClosing(QString) @fileChanged() @willDelete(PyQt_PyObject, PyQt_PyObject) @willOverWrite(PyQt_PyObject, QString, QString) @willMove(Qt_PyQtObject, QString, QString) @willSave(QString, QString) @savedAsNewFile(PyQt_PyObject, QString, QString) @gotAPath(PyQt_PyObject) @willAttachToExistingFile(PyQt_PyObject, QString) """ fileChanged = pyqtSignal() fileRemoved = pyqtSignal() fileReaded = pyqtSignal() willAttachToExistingFile = pyqtSignal('PyQt_PyObject', 'QString') gotAPath = pyqtSignal('PyQt_PyObject') willSave = pyqtSignal('QString', 'QString') willMove = pyqtSignal('PyQt_PyObject', 'QString', 'QString') willOverWrite = pyqtSignal('PyQt_PyObject', 'QString', 'QString') willCopyTo = pyqtSignal('PyQt_PyObject', 'QString', 'QString') willDelete = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject') fileClosing = pyqtSignal('QString', bool) def __init__(self, path=None): """ """ self._file_path = path self.__created = False self.__watcher = None self.__mtime = None super(NFile, self).__init__() if not self._exists(): self.__created = True @property def file_name(self): """"Returns filename of nfile""" file_name = None if self._file_path is None: file_name = translations.TR_NEW_DOCUMENT else: file_name = get_basename(self._file_path) return file_name @property def display_name(self): """Returns a pretty name to be displayed by tabs""" display_name = self.file_name if self._file_path is not None and not self.has_write_permission(): display_name += translations.TR_READ_ONLY return display_name @property def is_new_file(self): return self.__created def file_ext(self): """"Returns extension of nfile""" if self._file_path is None: return '' return get_file_extension(self._file_path) @property def file_path(self): """"Returns file path of nfile""" return self._file_path def start_watching(self): """Create a file system watcher and connect its fileChanged SIGNAL to our _file_changed SLOT""" if self.__watcher is None: self.__watcher = QFileSystemWatcher(self) self.__watcher.fileChanged['const QString&'].connect( self._file_changed) if self._file_path is not None: self.__mtime = os.path.getmtime(self._file_path) self.__watcher.addPath(self._file_path) def _file_changed(self, path): if self._exists(): current_mtime = os.path.getmtime(self._file_path) if current_mtime != self.__mtime: self.__mtime = current_mtime self.fileChanged.emit() else: self.fileRemoved.emit() def has_write_permission(self): if not self._exists(): return True return os.access(self._file_path, os.W_OK) def _exists(self): """ Check if we have been created with a path and if such path exists In case there is no path, we are most likely a new file. """ file_exists = False if self._file_path and os.path.exists(self._file_path): file_exists = True return file_exists def attach_to_path(self, new_path): if os.path.exists(new_path): signal_handler = SignalFlowControl() self.willAttachToExistingFile.emit(signal_handler, new_path) if signal_handler.stopped(): return self._file_path = new_path self.gotAPath.emit(self) return self._file_path def create(self): if self.__created: self.save("") self.__created = False def save(self, content, path=None): """ Write a temporary file with .tnj extension and copy it over the original one. .nsf = Ninja Swap File # FIXME: Where to locate addExtension, does not fit here """ new_path = False if path: self.attach_to_path(path) new_path = True save_path = self._file_path if not save_path: raise NinjaNoFileNameException("I am asked to write a " "file but no one told me where") swap_save_path = "%s.nsp" % save_path # If we have a file system watcher, remove the file path # from its watch list until we are done making changes. if self.__watcher is not None: self.__watcher.removePath(save_path) flags = QIODevice.WriteOnly | QIODevice.Truncate f = QFile(swap_save_path) if settings.use_platform_specific_eol(): flags |= QIODevice.Text if not f.open(flags): raise NinjaIOException(f.errorString()) stream = QTextStream(f) encoding = get_file_encoding(content) if encoding: stream.setCodec(encoding) encoded_stream = stream.codec().fromUnicode(content) f.write(encoded_stream) f.flush() f.close() # SIGNAL: Will save (temp, definitive) to warn folder to do something self.willSave.emit(swap_save_path, save_path) self.__mtime = os.path.getmtime(swap_save_path) shutil.move(swap_save_path, save_path) self.reset_state() # If we have a file system watcher, add the saved path back # to its watch list, otherwise create a watcher and start # watching if self.__watcher is not None: if new_path: # FIXME: what? # self.__watcher.removePath(self.__watcher.files()[0]) self.__watcher.addPath(self._file_path) else: self.__watcher.addPath(save_path) else: self.start_watching() return self def reset_state(self): """ #FIXE: to have a ref to changed I need to have the doc here """ self.__created = False def read(self, path=None): """ Read the file or fail """ open_path = path and path or self._file_path self._file_path = open_path if not self._file_path: raise NinjaNoFileNameException("I am asked to read a file " "but no one told me from where") try: with open(open_path, 'r') as f: content = f.read() except IOError as reason: raise NinjaIOException(reason) self.fileReaded.emit() return content def move(self, new_path): """ Phisically move the file """ if self._exists(): signal_handler = SignalFlowControl() # SIGNALL: WILL MOVE TO, to warn folder to exist self.willMove.emit(signal_handler, self._file_path, new_path) if signal_handler.stopped(): return if os.path.exists(new_path): signal_handler = SignalFlowControl() self.willOverWrite.emit(signal_handler, self._file_path, new_path) if signal_handler.stopped(): return if self.__watcher is not None: self.__watcher.removePath(self._file_path) shutil.move(self._file_path, new_path) if self.__watcher: self.__watcher.addPath(new_path) self._file_path = new_path return def copy(self, new_path): """ Copy the file to a new path """ if self._exists(): signal_handler = SignalFlowControl() # SIGNALL: WILL COPY TO, to warn folder to exist self.willCopyTo.emit(signal_handler, self._file_path, new_path) if signal_handler.stopped(): return if os.path.exists(new_path): signal_handler = SignalFlowControl() self.willOverWrite.emit(signal_handler, self._file_path, new_path) if signal_handler.stopped(): return shutil.copy(self._file_path, new_path) def delete(self, force=False): """ This deletes the object and closes the file. """ # if created but exists this file migth to someone else self.close() if ((not self.__created) or force) and self._exists(): DEBUG("Deleting our own NFile %s" % self._file_path) signal_handler = SignalFlowControl() self.willDelete.emit(signal_handler, self) if not signal_handler.stopped(): if self.__watcher is not None: self.__watcher.removePath(self._file_path) os.remove(self._file_path) def close(self, force_close=False): """ Lets let people know we are going down so they can act upon As you can see close does nothing but let everyone know that we are not saved yet """ DEBUG("About to close NFile") self.fileClosing.emit(self._file_path, force_close) def remove_watcher(self): if self.__watcher is not None: self.__watcher.removePath(self._file_path)
class Note(QWidget): def __init__(self, path, notebook): """ Class representing the actual tab pane for a note file, including the editor, the toolbar and the save/load logic. path is the path to the file(which does not need to exist yet) notebook must be the Notebook instance the note will be a part of """ QWidget.__init__(self) self.notebook = notebook self.path = path #Set up the embedded webkit self.edit = WebkitNoteEditor() self.edit.page().setContentEditable(True) self.edit.settings().setAttribute(QWebSettings.JavascriptEnabled, True) def openLink(url): decoded_url = urllib.parse.unquote_plus(url.toString()) try: if decoded_url.startswith("http"): #Note that we just pass the unmodifed url and assume the browser will handle that better webbrowser.open_new_tab(url.toString()) return if decoded_url.startswith("file://"): self.notebook.open(decoded_url[len("file://"):]) elif not decoded_url.startswith("mdnotes://"): self.notebook.open( os.path.join(os.path.dirname(self.path), decoded_url)) else: #Open knows how to handle these directly self.notebook.open(decoded_url) except: logging.exception("Failed to open link " + str(url.toString())) self.edit.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.edit.page().linkClicked.connect(openLink) #set up the toolbar self.tools = QWidget() self.tools.lo = QHBoxLayout() self.tools.setLayout(self.tools.lo) if self.path: #Watch the file so we can auto reload self.watcher = QFileSystemWatcher() self.watcher.addPath(self.path) self.watcher.fileChanged.connect(self.onChanged) #from http://ralsina.me/weblog/posts/BB948.html #Add a search feature self.search = QLineEdit( returnPressed=lambda: self.edit.findText(self.search.text())) self.search.hide() self.showSearch = QShortcut( "Ctrl+F", self, activated=lambda: (self.search.show(), self.search.setFocus())) #Put the widgets together self.lo = QVBoxLayout() self.setLayout(self.lo) self.tools = NoteToolBar(self) self.lo.addWidget(self.tools) self.lo.addWidget(self.edit) self.reload() def onClose(self): "Handle closing the tab or the whole program" self.watcher.removePath(self.path) self.save() def to_gfm(self): doc = pandoc.Document() doc.html = destyle_html( self.edit.page().mainFrame().toHtml()).encode("utf-8") return doc.markdown_github.decode("utf-8") def save(self, name=None): name = name or self.path #Readonly html file support if name.endswith(".ro"): return if name.endswith(".html"): return "Save the file if it needs saving" if not self.edit.page().isModified(): return #Back Up File buf = None #If the file exists, copy it to file~ first. If that exists, copy it to file4857475928345 if os.path.exists(name): if not os.path.exists(name + "~"): buf = (name + "~") shutil.copy(name, name + "~") else: buf = name + str(time.time()) shutil.copy(name, buf) #Again, pandoc to convert to the proper format doc = pandoc.Document() h = downloadAllImages(self.edit.page().mainFrame().toHtml(), name) doc.html = destyle_html(h).encode("utf-8") if util.striptrailingnumbers(name).endswith(".md"): with open(name, "wb") as f: f.write(self.pre_bytes + doc.markdown_github) if util.striptrailingnumbers(name).endswith(".rst"): with open(name, "wb") as f: f.write(self.pre_bytes + doc.rst) if buf and os.path.isfile(buf): os.remove(buf) #Reload to mark as saved, before the filesystem watcher ca get to it. self.reload() def onChanged(self): "Handle change in the filesystem" #Never let an external change completely destroy our work. self.save(self.path + str(time.time())) self.reload() #Three sleeps, really be sure the other process has put the file back. #See http://stackoverflow.com/questions/18300376/qt-qfilesystemwatcher-signal-filechanged-gets-emited-only-once time.sleep(0.01) time.sleep(0.01) time.sleep(0.01) self.watcher.addPath(self.path) def reload(self, dummy=True): "Reload the file from disk" if self.path.endswith('.jpg') or self.path.endswith( '.svg') or self.path.endswith('.png') or self.path.endswith( '.jpeg'): self.edit.setUrl(QUrl("file://" + self.path)) self.edit.page().setContentEditable(False) return if os.path.isfile(self.path): with open(self.path, "rb") as f: s = f.read() #Be compatible with files made on a certain android text editor that puts those bytes there sometimes. if s.startswith(b"\xff\xfe"): s = s[2:] self.pre_bytes = b"\xff\xfe" elif s.startswith(b"\xfe\xff"): s = s[2:] self.pre_bytes = b"\xfe\xff" elif s.startswith(b"\xef\xbb\xbf"): s = s[3:] self.pre_bytes = b"\xef\xbb\xbf" else: self.pre_bytes = b'' #now we are going to use pandoc to convert to html doc = pandoc.Document() #Figure out the input format. We back things up and archive them by appenting a Timestamp #to the end or else a ~. That function strips both of those things. if util.striptrailingnumbers(self.path).endswith(".html"): doc.html = s self.edit.page().setContentEditable(False) #The special html.ro lets us have read only HTML used for handy calculators and stuff. elif util.striptrailingnumbers(self.path).endswith(".html.ro"): doc.html = s self.edit.page().setContentEditable(False) elif util.striptrailingnumbers(self.path).endswith(".md"): doc.markdown_github = s elif util.striptrailingnumbers(self.path).endswith(".rst"): doc.rst = s else: raise RuntimeError("Bad filetype") html = doc.html.decode("utf-8") #Add the CSS file before the HTML d = "<style>" + styles.style + "</style>" self.header_size = len(d) d += html self.edit.setHtml( d, QUrl("file://" + self.path) if self.path else QUrl("file:///"))
class ReTextWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.resize(950, 700) screenRect = QDesktopWidget().screenGeometry() if globalSettings.windowGeometry: self.restoreGeometry(globalSettings.windowGeometry) else: self.move((screenRect.width() - self.width()) // 2, (screenRect.height() - self.height()) // 2) if not screenRect.contains(self.geometry()): self.showMaximized() if sys.platform.startswith('darwin'): # https://github.com/retext-project/retext/issues/198 searchPaths = QIcon.themeSearchPaths() searchPaths.append('/opt/local/share/icons') searchPaths.append('/usr/local/share/icons') QIcon.setThemeSearchPaths(searchPaths) setIconThemeFromSettings() if QFile.exists(getBundledIcon('retext')): self.setWindowIcon(QIcon(getBundledIcon('retext'))) elif QFile.exists('/usr/share/pixmaps/retext.png'): self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png')) else: self.setWindowIcon(QIcon.fromTheme('retext', QIcon.fromTheme('accessories-text-editor'))) self.tabWidget = QTabWidget(self) self.initTabWidget() self.setCentralWidget(self.tabWidget) self.tabWidget.currentChanged.connect(self.changeIndex) self.tabWidget.tabCloseRequested.connect(self.closeTab) self.toolBar = QToolBar(self.tr('File toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.toolBar) self.editBar = QToolBar(self.tr('Edit toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.editBar) self.searchBar = QToolBar(self.tr('Search toolbar'), self) self.addToolBar(Qt.BottomToolBarArea, self.searchBar) self.toolBar.setVisible(not globalSettings.hideToolBar) self.editBar.setVisible(not globalSettings.hideToolBar) self.actionNew = self.act(self.tr('New'), 'document-new', self.createNew, shct=QKeySequence.New) self.actionNew.setPriority(QAction.LowPriority) self.actionOpen = self.act(self.tr('Open'), 'document-open', self.openFile, shct=QKeySequence.Open) self.actionOpen.setPriority(QAction.LowPriority) self.actionSetEncoding = self.act(self.tr('Set encoding'), trig=self.showEncodingDialog) self.actionSetEncoding.setEnabled(False) self.actionReload = self.act(self.tr('Reload'), 'view-refresh', lambda: self.currentTab.readTextFromFile()) self.actionReload.setEnabled(False) self.actionSave = self.act(self.tr('Save'), 'document-save', self.saveFile, shct=QKeySequence.Save) self.actionSave.setEnabled(False) self.actionSave.setPriority(QAction.LowPriority) self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as', self.saveFileAs, shct=QKeySequence.SaveAs) self.actionNextTab = self.act(self.tr('Next tab'), 'go-next', lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown) self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous', lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp) self.actionCloseCurrentTab = self.act(self.tr('Close tab'), 'window-close', lambda: self.closeTab(self.ind), shct=QKeySequence.Close) self.actionPrint = self.act(self.tr('Print'), 'document-print', self.printFile, shct=QKeySequence.Print) self.actionPrint.setPriority(QAction.LowPriority) self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview', self.printPreview) self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml) self.actionChangeEditorFont = self.act(self.tr('Change editor font'), trig=self.changeEditorFont) self.actionChangePreviewFont = self.act(self.tr('Change preview font'), trig=self.changePreviewFont) self.actionSearch = self.act(self.tr('Find text'), 'edit-find', self.search, shct=QKeySequence.Find) self.actionGoToLine = self.act(self.tr('Go to line'), trig=self.goToLine, shct=Qt.CTRL+Qt.Key_G) self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged) self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E, trigbool=self.preview) if QIcon.hasThemeIcon('document-preview'): self.actionPreview.setIcon(QIcon.fromTheme('document-preview')) elif QIcon.hasThemeIcon('preview-file'): self.actionPreview.setIcon(QIcon.fromTheme('preview-file')) elif QIcon.hasThemeIcon('x-office-document'): self.actionPreview.setIcon(QIcon.fromTheme('x-office-document')) else: self.actionPreview.setIcon(QIcon(getBundledIcon('document-preview'))) self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L, trigbool=self.enableLivePreview) menuPreview = QMenu() menuPreview.addAction(self.actionLivePreview) self.actionPreview.setMenu(menuPreview) self.actionInsertTable = self.act(self.tr('Insert table'), trig=lambda: self.insertFormatting('table')) self.actionTableMode = self.act(self.tr('Table editing mode'), shct=Qt.CTRL+Qt.Key_T, trigbool=lambda x: self.currentTab.editBox.enableTableMode(x)) self.actionInsertImages = self.act(self.tr('Insert images by file path'), trig=lambda: self.insertImages()) if ReTextFakeVimHandler: self.actionFakeVimMode = self.act(self.tr('FakeVim mode'), shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode) if globalSettings.useFakeVim: self.actionFakeVimMode.setChecked(True) self.enableFakeVimMode(True) self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen', shct=Qt.Key_F11, trigbool=self.enableFullScreen) self.actionFullScreen.setChecked(self.isFullScreen()) self.actionFullScreen.setPriority(QAction.LowPriority) self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system', trig=self.openConfigDialog) self.actionConfig.setMenuRole(QAction.PreferencesRole) self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml) self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf) self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf) self.getExportExtensionsList() self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit) self.actionQuit.setMenuRole(QAction.QuitRole) self.actionQuit.triggered.connect(self.close) self.actionUndo = self.act(self.tr('Undo'), 'edit-undo', lambda: self.currentTab.editBox.undo(), shct=QKeySequence.Undo) self.actionRedo = self.act(self.tr('Redo'), 'edit-redo', lambda: self.currentTab.editBox.redo(), shct=QKeySequence.Redo) self.actionCopy = self.act(self.tr('Copy'), 'edit-copy', lambda: self.currentTab.editBox.copy(), shct=QKeySequence.Copy) self.actionCut = self.act(self.tr('Cut'), 'edit-cut', lambda: self.currentTab.editBox.cut(), shct=QKeySequence.Cut) self.actionPaste = self.act(self.tr('Paste'), 'edit-paste', lambda: self.currentTab.editBox.paste(), shct=QKeySequence.Paste) self.actionPasteImage = self.act(self.tr('Paste image'), 'edit-paste', lambda: self.currentTab.editBox.pasteImage(), shct=Qt.CTRL+Qt.SHIFT+Qt.Key_V) self.actionMoveUp = self.act(self.tr('Move line up'), 'go-up', lambda: self.currentTab.editBox.moveLineUp(), shct=Qt.ALT+Qt.Key_Up) self.actionMoveDown = self.act(self.tr('Move line down'), 'go-down', lambda: self.currentTab.editBox.moveLineDown(), shct=Qt.ALT+Qt.Key_Down) self.actionUndo.setEnabled(False) self.actionRedo.setEnabled(False) self.actionCopy.setEnabled(False) self.actionCut.setEnabled(False) qApp = QApplication.instance() qApp.clipboard().dataChanged.connect(self.clipboardDataChanged) self.clipboardDataChanged() if enchant is not None: self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck) self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale) self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit) if ReTextWebKitPreview is None: globalSettings.useWebKit = False self.actionWebKit.setEnabled(False) self.actionWebKit.setChecked(globalSettings.useWebKit) self.actionWebEngine = self.act(self.tr('Use WebEngine (Chromium) renderer'), trigbool=self.enableWebEngine) if ReTextWebEnginePreview is None: globalSettings.useWebEngine = False self.actionWebEngine.setChecked(globalSettings.useWebEngine) self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir) self.actionFind = self.act(self.tr('Next'), 'go-next', self.find, shct=QKeySequence.FindNext) self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous', lambda: self.find(back=True), shct=QKeySequence.FindPrevious) self.actionReplace = self.act(self.tr('Replace'), 'edit-find-replace', lambda: self.find(replace=True)) self.actionReplaceAll = self.act(self.tr('Replace all'), trig=self.replaceAll) menuReplace = QMenu() menuReplace.addAction(self.actionReplaceAll) self.actionReplace.setMenu(menuReplace) self.actionCloseSearch = self.act(self.tr('Close'), 'window-close', lambda: self.searchBar.setVisible(False), shct=QKeySequence.Cancel) self.actionCloseSearch.setPriority(QAction.LowPriority) self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp) self.aboutWindowTitle = self.tr('About ReText') self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog) self.actionAbout.setMenuRole(QAction.AboutRole) self.actionAboutQt = self.act(self.tr('About Qt')) self.actionAboutQt.setMenuRole(QAction.AboutQtRole) self.actionAboutQt.triggered.connect(qApp.aboutQt) availableMarkups = markups.get_available_markups() if not availableMarkups: print('Warning: no markups are available!') if len(availableMarkups) > 1: self.chooseGroup = QActionGroup(self) markupActions = [] for markup in availableMarkups: markupAction = self.act(markup.name, trigbool=self.markupFunction(markup)) if markup.name == globalSettings.defaultMarkup: markupAction.setChecked(True) self.chooseGroup.addAction(markupAction) markupActions.append(markupAction) self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold, trig=lambda: self.insertFormatting('bold')) self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic, trig=lambda: self.insertFormatting('italic')) self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline, trig=lambda: self.insertFormatting('underline')) self.usefulTags = ('header', 'italic', 'bold', 'underline', 'numbering', 'bullets', 'image', 'link', 'inline code', 'code block', 'blockquote', 'table') self.usefulChars = ('deg', 'divide', 'euro', 'hellip', 'laquo', 'larr', 'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo', 'rarr', 'rsquo', 'times') self.formattingBox = QComboBox(self.editBar) self.formattingBox.addItem(self.tr('Formatting')) self.formattingBox.addItems(self.usefulTags) self.formattingBox.activated[str].connect(self.insertFormatting) self.symbolBox = QComboBox(self.editBar) self.symbolBox.addItem(self.tr('Symbols')) self.symbolBox.addItems(self.usefulChars) self.symbolBox.activated.connect(self.insertSymbol) self.updateStyleSheet() menubar = self.menuBar() menuFile = menubar.addMenu(self.tr('File')) menuEdit = menubar.addMenu(self.tr('Edit')) menuHelp = menubar.addMenu(self.tr('Help')) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent')) self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles) menuFile.addAction(self.actionShow) menuFile.addAction(self.actionSetEncoding) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addSeparator() menuFile.addAction(self.actionNextTab) menuFile.addAction(self.actionPrevTab) menuFile.addAction(self.actionCloseCurrentTab) menuFile.addSeparator() menuExport = menuFile.addMenu(self.tr('Export')) menuExport.addAction(self.actionSaveHtml) menuExport.addAction(self.actionOdf) menuExport.addAction(self.actionPdf) if self.extensionActions: menuExport.addSeparator() for action, mimetype in self.extensionActions: menuExport.addAction(action) menuExport.aboutToShow.connect(self.updateExtensionsVisibility) menuFile.addAction(self.actionPrint) menuFile.addAction(self.actionPrintPreview) menuFile.addSeparator() menuFile.addAction(self.actionQuit) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addAction(self.actionPasteImage) menuEdit.addSeparator() menuEdit.addAction(self.actionMoveUp) menuEdit.addAction(self.actionMoveDown) menuEdit.addSeparator() if enchant is not None: menuSC = menuEdit.addMenu(self.tr('Spell check')) menuSC.addAction(self.actionEnableSC) menuSC.addAction(self.actionSetLocale) menuEdit.addAction(self.actionSearch) menuEdit.addAction(self.actionGoToLine) menuEdit.addAction(self.actionChangeEditorFont) menuEdit.addAction(self.actionChangePreviewFont) menuEdit.addSeparator() if len(availableMarkups) > 1: self.menuMode = menuEdit.addMenu(self.tr('Default markup')) for markupAction in markupActions: self.menuMode.addAction(markupAction) menuFormat = menuEdit.addMenu(self.tr('Formatting')) menuFormat.addAction(self.actionBold) menuFormat.addAction(self.actionItalic) menuFormat.addAction(self.actionUnderline) if ReTextWebKitPreview is not None or ReTextWebEnginePreview is None: menuEdit.addAction(self.actionWebKit) else: menuEdit.addAction(self.actionWebEngine) menuEdit.addSeparator() menuEdit.addAction(self.actionViewHtml) menuEdit.addAction(self.actionPreview) menuEdit.addAction(self.actionInsertTable) menuEdit.addAction(self.actionTableMode) menuEdit.addAction(self.actionInsertImages) if ReTextFakeVimHandler: menuEdit.addAction(self.actionFakeVimMode) menuEdit.addSeparator() menuEdit.addAction(self.actionFullScreen) menuEdit.addAction(self.actionConfig) menuHelp.addAction(self.actionHelp) menuHelp.addSeparator() menuHelp.addAction(self.actionAbout) menuHelp.addAction(self.actionAboutQt) self.toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.toolBar.addAction(self.actionNew) self.toolBar.addSeparator() self.toolBar.addAction(self.actionOpen) self.toolBar.addAction(self.actionSave) self.toolBar.addAction(self.actionPrint) self.toolBar.addSeparator() self.toolBar.addAction(self.actionPreview) self.toolBar.addAction(self.actionFullScreen) self.editBar.addAction(self.actionUndo) self.editBar.addAction(self.actionRedo) self.editBar.addSeparator() self.editBar.addAction(self.actionCut) self.editBar.addAction(self.actionCopy) self.editBar.addAction(self.actionPaste) self.editBar.addSeparator() self.editBar.addWidget(self.formattingBox) self.editBar.addWidget(self.symbolBox) self.searchEdit = QLineEdit(self.searchBar) self.searchEdit.setPlaceholderText(self.tr('Search')) self.searchEdit.returnPressed.connect(self.find) self.replaceEdit = QLineEdit(self.searchBar) self.replaceEdit.setPlaceholderText(self.tr('Replace with')) self.replaceEdit.returnPressed.connect(self.find) self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar) self.searchBar.addWidget(self.searchEdit) self.searchBar.addWidget(self.replaceEdit) self.searchBar.addSeparator() self.searchBar.addWidget(self.csBox) self.searchBar.addAction(self.actionFindPrev) self.searchBar.addAction(self.actionFind) self.searchBar.addAction(self.actionReplace) self.searchBar.addAction(self.actionCloseSearch) self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.searchBar.setVisible(False) self.autoSaveEnabled = globalSettings.autoSave if self.autoSaveEnabled: timer = QTimer(self) timer.start(60000) timer.timeout.connect(self.saveAll) self.ind = None if enchant is not None: self.sl = globalSettings.spellCheckLocale try: enchant.Dict(self.sl or None) except enchant.errors.Error as e: warnings.warn(str(e), RuntimeWarning) globalSettings.spellCheck = False if globalSettings.spellCheck: self.actionEnableSC.setChecked(True) self.fileSystemWatcher = QFileSystemWatcher() self.fileSystemWatcher.fileChanged.connect(self.fileChanged) def restoreLastOpenedFiles(self): for file in readListFromSettings("lastFileList"): self.openFileWrapper(file) # Show the tab of last opened file lastTabIndex = globalSettings.lastTabIndex if lastTabIndex >= 0 and lastTabIndex < self.tabWidget.count(): self.tabWidget.setCurrentIndex(lastTabIndex) def iterateTabs(self): for i in range(self.tabWidget.count()): yield self.tabWidget.widget(i) def updateStyleSheet(self): self.ss = None if globalSettings.styleSheet: sheetfile = QFile(globalSettings.styleSheet) sheetfile.open(QIODevice.ReadOnly) self.ss = QTextStream(sheetfile).readAll() sheetfile.close() def initTabWidget(self): def dragEnterEvent(e): e.acceptProposedAction() def dropEvent(e): fn = bytes(e.mimeData().data('text/plain')).decode().rstrip() if fn.startswith('file:'): fn = QUrl(fn).toLocalFile() self.openFileWrapper(fn) self.tabWidget.setTabsClosable(True) self.tabWidget.setAcceptDrops(True) self.tabWidget.setMovable(True) self.tabWidget.dragEnterEvent = dragEnterEvent self.tabWidget.dropEvent = dropEvent self.tabWidget.setTabBarAutoHide(globalSettings.tabBarAutoHide) def act(self, name, icon=None, trig=None, trigbool=None, shct=None): if not isinstance(shct, QKeySequence): shct = QKeySequence(shct) if icon: action = QAction(self.actIcon(icon), name, self) else: action = QAction(name, self) if trig: action.triggered.connect(trig) elif trigbool: action.setCheckable(True) action.triggered[bool].connect(trigbool) if shct: action.setShortcut(shct) return action def actIcon(self, name): return QIcon.fromTheme(name, QIcon(getBundledIcon(name))) def printError(self): import traceback print('Exception occurred while parsing document:', file=sys.stderr) traceback.print_exc() def updateTabTitle(self, ind, tab): changed = tab.editBox.document().isModified() if changed and not self.autoSaveActive(tab): title = tab.getBaseName() + '*' else: title = tab.getBaseName() self.tabWidget.setTabText(ind, title) def tabFileNameChanged(self, tab): ''' Perform all UI state changes that need to be done when the filename of the current tab has changed. ''' if tab == self.currentTab: if tab.fileName: self.setWindowTitle("") if globalSettings.windowTitleFullPath: self.setWindowTitle(tab.fileName + '[*]') self.setWindowFilePath(tab.fileName) self.updateTabTitle(self.ind, tab) self.tabWidget.setTabToolTip(self.ind, tab.fileName) QDir.setCurrent(QFileInfo(tab.fileName).dir().path()) else: self.setWindowFilePath('') self.setWindowTitle(self.tr('New document') + '[*]') canReload = bool(tab.fileName) and not self.autoSaveActive(tab) self.actionSetEncoding.setEnabled(canReload) self.actionReload.setEnabled(canReload) def tabActiveMarkupChanged(self, tab): ''' Perform all UI state changes that need to be done when the active markup class of the current tab has changed. ''' if tab == self.currentTab: markupClass = tab.getActiveMarkupClass() dtMarkdown = (markupClass == markups.MarkdownMarkup) dtMkdOrReST = dtMarkdown or (markupClass == markups.ReStructuredTextMarkup) self.formattingBox.setEnabled(dtMarkdown) self.symbolBox.setEnabled(dtMarkdown) self.actionUnderline.setEnabled(dtMarkdown) self.actionBold.setEnabled(dtMkdOrReST) self.actionItalic.setEnabled(dtMkdOrReST) def tabModificationStateChanged(self, tab): ''' Perform all UI state changes that need to be done when the modification state of the current tab has changed. ''' if tab == self.currentTab: changed = tab.editBox.document().isModified() if self.autoSaveActive(tab): changed = False self.actionSave.setEnabled(changed) self.updateTabTitle(self.ind, tab) self.setWindowModified(changed) def createTab(self, fileName): previewStatesByName = { 'editor': PreviewDisabled, 'normal-preview': PreviewNormal, 'live-preview': PreviewLive, } previewState = previewStatesByName.get(globalSettings.defaultPreviewState, PreviewDisabled) if previewState == PreviewNormal and not fileName: previewState = PreviewDisabled # Opening empty document in preview mode makes no sense self.currentTab = ReTextTab(self, fileName, previewState) self.currentTab.fileNameChanged.connect(lambda: self.tabFileNameChanged(self.currentTab)) self.currentTab.modificationStateChanged.connect(lambda: self.tabModificationStateChanged(self.currentTab)) self.currentTab.activeMarkupChanged.connect(lambda: self.tabActiveMarkupChanged(self.currentTab)) self.tabWidget.addTab(self.currentTab, self.tr("New document")) self.currentTab.updateBoxesVisibility() if previewState > 0: QTimer.singleShot(500, self.currentTab.triggerPreviewUpdate) def closeTab(self, ind): if self.maybeSave(ind): if self.tabWidget.count() == 1: self.createTab("") closedTab = self.tabWidget.widget(ind) if closedTab.fileName: self.fileSystemWatcher.removePath(closedTab.fileName) self.tabWidget.removeTab(ind) closedTab.deleteLater() def changeIndex(self, ind): ''' This function is called when a different tab is selected. It changes the state of the window to mirror the current state of the newly selected tab. Future changes to this state will be done in response to signals emitted by the tab, to which the window was subscribed when the tab was created. The window is subscribed to all tabs like this, but only the active tab will logically generate these signals. Aside from the above this function also calls the handlers for the other changes that are implied by a tab switch: filename change, modification state change and active markup change. ''' self.currentTab = self.tabWidget.currentWidget() editBox = self.currentTab.editBox previewState = self.currentTab.previewState self.actionUndo.setEnabled(editBox.document().isUndoAvailable()) self.actionRedo.setEnabled(editBox.document().isRedoAvailable()) self.actionCopy.setEnabled(editBox.textCursor().hasSelection()) self.actionCut.setEnabled(editBox.textCursor().hasSelection()) self.actionPreview.setChecked(previewState >= PreviewLive) self.actionLivePreview.setChecked(previewState == PreviewLive) self.actionTableMode.setChecked(editBox.tableModeEnabled) self.editBar.setEnabled(previewState < PreviewNormal) self.ind = ind editBox.setFocus(Qt.OtherFocusReason) self.tabFileNameChanged(self.currentTab) self.tabModificationStateChanged(self.currentTab) self.tabActiveMarkupChanged(self.currentTab) def changeEditorFont(self): font, ok = QFontDialog.getFont(globalSettings.editorFont, self) if ok: self.setEditorFont(font) def setEditorFont(self, font): globalSettings.editorFont = font for tab in self.iterateTabs(): tab.editBox.updateFont() def changePreviewFont(self): font, ok = QFontDialog.getFont(globalSettings.font, self) if ok: self.setPreviewFont(font) def setPreviewFont(self, font): globalSettings.font = font for tab in self.iterateTabs(): tab.triggerPreviewUpdate() def preview(self, viewmode): self.currentTab.previewState = viewmode * 2 self.actionLivePreview.setChecked(False) self.editBar.setDisabled(viewmode) self.currentTab.updateBoxesVisibility() self.currentTab.triggerPreviewUpdate() def enableLivePreview(self, livemode): self.currentTab.previewState = int(livemode) self.actionPreview.setChecked(livemode) self.editBar.setEnabled(True) self.currentTab.updateBoxesVisibility() self.currentTab.triggerPreviewUpdate() def enableWebKit(self, enable): globalSettings.useWebKit = enable globalSettings.useWebEngine = False for tab in self.iterateTabs(): tab.rebuildPreviewBox() def enableWebEngine(self, enable): globalSettings.useWebKit = False globalSettings.useWebEngine = enable for tab in self.iterateTabs(): tab.rebuildPreviewBox() def enableCopy(self, copymode): self.actionCopy.setEnabled(copymode) self.actionCut.setEnabled(copymode) def enableFullScreen(self, yes): if yes: self.showFullScreen() else: self.showNormal() def openConfigDialog(self): dlg = ConfigDialog(self) dlg.setWindowTitle(self.tr('Preferences')) dlg.show() def enableFakeVimMode(self, yes): globalSettings.useFakeVim = yes if yes: FakeVimMode.init(self) for tab in self.iterateTabs(): tab.editBox.installFakeVimHandler() else: FakeVimMode.exit(self) def enableSpellCheck(self, yes): try: dict = enchant.Dict(self.sl or None) except enchant.errors.Error as e: QMessageBox.warning(self, '', str(e)) self.actionEnableSC.setChecked(False) yes = False self.setAllDictionaries(dict if yes else None) globalSettings.spellCheck = yes def setAllDictionaries(self, dictionary): for tab in self.iterateTabs(): hl = tab.highlighter hl.dictionary = dictionary hl.rehighlight() def changeLocale(self): localedlg = LocaleDialog(self, defaultText=self.sl) if localedlg.exec() != QDialog.Accepted: return sl = localedlg.localeEdit.text() try: enchant.Dict(sl or None) except enchant.errors.Error as e: QMessageBox.warning(self, '', str(e)) else: self.sl = sl or None self.enableSpellCheck(self.actionEnableSC.isChecked()) if localedlg.checkBox.isChecked(): globalSettings.spellCheckLocale = sl def search(self): self.searchBar.setVisible(True) self.searchEdit.setFocus(Qt.ShortcutFocusReason) def goToLine(self): line, ok = QInputDialog.getInt(self, self.tr("Go to line"), self.tr("Type the line number")) if ok: self.currentTab.goToLine(line-1) def searchBarVisibilityChanged(self, visible): if visible: self.searchEdit.setFocus(Qt.ShortcutFocusReason) def find(self, back=False, replace=False): flags = QTextDocument.FindFlags() if back: flags |= QTextDocument.FindBackward if self.csBox.isChecked(): flags |= QTextDocument.FindCaseSensitively text = self.searchEdit.text() replaceText = self.replaceEdit.text() if replace else None found = self.currentTab.find(text, flags, replaceText=replaceText) self.setSearchEditColor(found) def replaceAll(self): text = self.searchEdit.text() replaceText = self.replaceEdit.text() found = self.currentTab.replaceAll(text, replaceText) self.setSearchEditColor(found) def setSearchEditColor(self, found): palette = self.searchEdit.palette() palette.setColor(QPalette.Active, QPalette.Base, Qt.white if found else QColor(255, 102, 102)) self.searchEdit.setPalette(palette) def showInDir(self): if self.currentTab.fileName: path = QFileInfo(self.currentTab.fileName).path() QDesktopServices.openUrl(QUrl.fromLocalFile(path)) else: QMessageBox.warning(self, '', self.tr("Please, save the file somewhere.")) def moveToTopOfRecentFileList(self, fileName): if fileName: files = readListFromSettings("recentFileList") if fileName in files: files.remove(fileName) files.insert(0, fileName) recentCount = globalSettings.recentDocumentsCount if len(files) > recentCount: del files[recentCount:] writeListToSettings("recentFileList", files) def createNew(self, text=None): self.createTab("") self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if text: self.currentTab.editBox.textCursor().insertText(text) def switchTab(self, shift=1): self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count()) def updateRecentFiles(self): self.menuRecentFiles.clear() self.recentFilesActions = [] filesOld = readListFromSettings("recentFileList") files = [] for f in filesOld: if QFile.exists(f): files.append(f) self.recentFilesActions.append(self.act(f, trig=self.openFunction(f))) writeListToSettings("recentFileList", files) for action in self.recentFilesActions: self.menuRecentFiles.addAction(action) def markupFunction(self, markup): return lambda: self.setDefaultMarkup(markup) def openFunction(self, fileName): return lambda: self.openFileWrapper(fileName) def extensionFunction(self, data): return lambda: \ self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension']) def getExportExtensionsList(self): extensions = [] for extsprefix in datadirs: extsdir = QDir(extsprefix+'/export-extensions/') if extsdir.exists(): for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], QDir.Files | QDir.Readable): extensions.append(self.readExtension(fileInfo.filePath())) locale = QLocale.system().name() self.extensionActions = [] for extension in extensions: try: if ('Name[%s]' % locale) in extension: name = extension['Name[%s]' % locale] elif ('Name[%s]' % locale.split('_')[0]) in extension: name = extension['Name[%s]' % locale.split('_')[0]] else: name = extension['Name'] data = {} for prop in ('FileFilter', 'DefaultExtension', 'Exec'): if 'X-ReText-'+prop in extension: data[prop] = extension['X-ReText-'+prop] elif prop in extension: data[prop] = extension[prop] else: data[prop] = '' action = self.act(name, trig=self.extensionFunction(data)) if 'Icon' in extension: action.setIcon(self.actIcon(extension['Icon'])) mimetype = extension['MimeType'] if 'MimeType' in extension else None except KeyError: print('Failed to parse extension: Name is required', file=sys.stderr) else: self.extensionActions.append((action, mimetype)) def updateExtensionsVisibility(self): markupClass = self.currentTab.getActiveMarkupClass() for action in self.extensionActions: if markupClass is None: action[0].setEnabled(False) continue mimetype = action[1] if mimetype is None: enabled = True elif markupClass == markups.MarkdownMarkup: enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown", "text/markdown")) elif markupClass == markups.ReStructuredTextMarkup: enabled = (mimetype in ("text/x-retext-rst", "text/x-rst")) else: enabled = False action[0].setEnabled(enabled) def readExtension(self, fileName): extFile = QFile(fileName) extFile.open(QIODevice.ReadOnly) extension = {} stream = QTextStream(extFile) while not stream.atEnd(): line = stream.readLine() if '=' in line: index = line.index('=') extension[line[:index].rstrip()] = line[index+1:].lstrip() extFile.close() return extension def openFile(self): supportedExtensions = ['.txt'] for markup in markups.get_all_markups(): supportedExtensions += markup.file_extensions fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;' fileNames = QFileDialog.getOpenFileNames(self, self.tr("Select one or several files to open"), QDir.currentPath(), self.tr("Supported files") + fileFilter + self.tr("All files (*)")) for fileName in fileNames[0]: self.openFileWrapper(fileName) @pyqtSlot(str) def openFileWrapper(self, fileName): if not fileName: return fileName = QFileInfo(fileName).canonicalFilePath() exists = False for i, tab in enumerate(self.iterateTabs()): if tab.fileName == fileName: exists = True ex = i if exists: self.tabWidget.setCurrentIndex(ex) elif QFile.exists(fileName): noEmptyTab = ( (self.ind is None) or self.currentTab.fileName or self.currentTab.editBox.toPlainText() or self.currentTab.editBox.document().isModified() ) if noEmptyTab: self.createTab(fileName) self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if fileName: self.fileSystemWatcher.addPath(fileName) self.currentTab.readTextFromFile(fileName) self.moveToTopOfRecentFileList(self.currentTab.fileName) def showEncodingDialog(self): if not self.maybeSave(self.ind): return codecsSet = set(bytes(QTextCodec.codecForName(alias).name()) for alias in QTextCodec.availableCodecs()) encoding, ok = QInputDialog.getItem(self, '', self.tr('Select file encoding from the list:'), [bytes(b).decode() for b in sorted(codecsSet)], 0, False) if ok: self.currentTab.readTextFromFile(None, encoding) def saveFileAs(self): self.saveFile(dlg=True) def saveAll(self): for tab in self.iterateTabs(): if (tab.fileName and tab.editBox.document().isModified() and QFileInfo(tab.fileName).isWritable()): tab.saveTextToFile() def saveFile(self, dlg=False): fileNameToSave = self.currentTab.fileName if (not fileNameToSave) or dlg: proposedFileName = "" markupClass = self.currentTab.getActiveMarkupClass() if (markupClass is None) or not hasattr(markupClass, 'default_extension'): defaultExt = self.tr("Plain text (*.txt)") ext = ".txt" else: defaultExt = self.tr('%s files', 'Example of final string: Markdown files') \ % markupClass.name + ' (' + str.join(' ', ('*'+extension for extension in markupClass.file_extensions)) + ')' if markupClass == markups.MarkdownMarkup: ext = globalSettings.markdownDefaultFileExtension elif markupClass == markups.ReStructuredTextMarkup: ext = globalSettings.restDefaultFileExtension else: ext = markupClass.default_extension if fileNameToSave is not None: proposedFileName = fileNameToSave fileNameToSave = QFileDialog.getSaveFileName(self, self.tr("Save file"), proposedFileName, defaultExt)[0] if fileNameToSave: if not QFileInfo(fileNameToSave).suffix(): fileNameToSave += ext # Make sure we don't overwrite a file opened in other tab for tab in self.iterateTabs(): if tab is not self.currentTab and tab.fileName == fileNameToSave: QMessageBox.warning(self, "", self.tr("Cannot save to file which is open in another tab!")) return False self.actionSetEncoding.setDisabled(self.autoSaveActive()) if fileNameToSave: if self.currentTab.saveTextToFile(fileNameToSave): self.moveToTopOfRecentFileList(self.currentTab.fileName) return True else: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return False def saveHtml(self, fileName): if not QFileInfo(fileName).suffix(): fileName += ".html" try: _, htmltext, _ = self.currentTab.getDocumentForExport(webenv=True) except Exception: return self.printError() htmlFile = QFile(fileName) result = htmlFile.open(QIODevice.WriteOnly) if not result: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return html = QTextStream(htmlFile) if globalSettings.defaultCodec: html.setCodec(globalSettings.defaultCodec) html << htmltext htmlFile.close() def textDocument(self, title, htmltext): td = QTextDocument() td.setMetaInformation(QTextDocument.DocumentTitle, title) td.setHtml(htmltext) td.setDefaultFont(globalSettings.font) return td def saveOdf(self): title, htmltext, _ = self.currentTab.getDocumentForExport() try: document = self.textDocument(title, htmltext) except Exception: return self.printError() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to ODT"), self.currentTab.getBaseName() + ".odt", self.tr("OpenDocument text files (*.odt)"))[0] if not QFileInfo(fileName).suffix(): fileName += ".odt" writer = QTextDocumentWriter(fileName) writer.setFormat(b"odf") writer.write(document) def saveFileHtml(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), self.currentTab.getBaseName() + ".html", self.tr("HTML files (*.html *.htm)"))[0] if fileName: self.saveHtml(fileName) def getDocumentForPrint(self, title, htmltext, preview): if globalSettings.useWebKit: return preview try: return self.textDocument(title, htmltext) except Exception: self.printError() def standardPrinter(self, title): printer = QPrinter(QPrinter.HighResolution) printer.setDocName(title) printer.setCreator('ReText %s' % app_version) if globalSettings.paperSize: pageSize = self.getPageSizeByName(globalSettings.paperSize) if pageSize is not None: printer.setPaperSize(pageSize) else: QMessageBox.warning(self, '', self.tr('Unrecognized paperSize setting "%s".') % globalSettings.paperSize) return printer def getPageSizeByName(self, pageSizeName): """ Returns a validated PageSize instance corresponding to the given name. Returns None if the name is not a valid PageSize. """ pageSize = None lowerCaseNames = {pageSize.lower(): pageSize for pageSize in self.availablePageSizes()} if pageSizeName.lower() in lowerCaseNames: pageSize = getattr(QPagedPaintDevice, lowerCaseNames[pageSizeName.lower()]) return pageSize def availablePageSizes(self): """ List available page sizes. """ sizes = [x for x in dir(QPagedPaintDevice) if type(getattr(QPagedPaintDevice, x)) == QPagedPaintDevice.PageSize] return sizes def savePdf(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to PDF"), self.currentTab.getBaseName() + ".pdf", self.tr("PDF files (*.pdf)"))[0] if fileName: if not QFileInfo(fileName).suffix(): fileName += ".pdf" title, htmltext, preview = self.currentTab.getDocumentForExport() if globalSettings.useWebEngine and hasattr(preview.page(), "printToPdf"): pageSize = self.getPageSizeByName(globalSettings.paperSize) if pageSize is None: pageSize = QPageSize(QPageSize.A4) margins = QMarginsF(20, 20, 13, 20) # left, top, right, bottom (in millimeters) layout = QPageLayout(pageSize, QPageLayout.Portrait, margins, QPageLayout.Millimeter) preview.page().printToPdf(fileName, layout) # Available since Qt 5.7 return printer = self.standardPrinter(title) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fileName) document = self.getDocumentForPrint(title, htmltext, preview) if document != None: document.print(printer) def printFile(self): title, htmltext, preview = self.currentTab.getDocumentForExport() printer = self.standardPrinter(title) dlg = QPrintDialog(printer, self) dlg.setWindowTitle(self.tr("Print document")) if (dlg.exec() == QDialog.Accepted): document = self.getDocumentForPrint(title, htmltext, preview) if document != None: document.print(printer) def printPreview(self): title, htmltext, preview = self.currentTab.getDocumentForExport() document = self.getDocumentForPrint(title, htmltext, preview) if document is None: return printer = self.standardPrinter(title) preview = QPrintPreviewDialog(printer, self) preview.paintRequested.connect(document.print) preview.exec() def runExtensionCommand(self, command, filefilter, defaultext): import shlex of = ('%of' in command) html = ('%html' in command) if of: if defaultext and not filefilter: filefilter = '*'+defaultext fileName = QFileDialog.getSaveFileName(self, self.tr('Export document'), '', filefilter)[0] if not fileName: return if defaultext and not QFileInfo(fileName).suffix(): fileName += defaultext else: fileName = 'out' + defaultext basename = '.%s.retext-temp' % self.currentTab.getBaseName() if html: tmpname = basename+'.html' self.saveHtml(tmpname) else: tmpname = basename + self.currentTab.getActiveMarkupClass().default_extension self.currentTab.writeTextToFile(tmpname) command = command.replace('%of', shlex.quote(fileName)) command = command.replace('%html' if html else '%if', shlex.quote(tmpname)) try: Popen(str(command), shell=True).wait() except Exception as error: errorstr = str(error) QMessageBox.warning(self, '', self.tr('Failed to execute the command:') + '\n' + errorstr) QFile(tmpname).remove() def autoSaveActive(self, tab=None): tab = tab if tab else self.currentTab return bool(self.autoSaveEnabled and tab.fileName and QFileInfo(tab.fileName).isWritable()) def clipboardDataChanged(self): mimeData = QApplication.instance().clipboard().mimeData() if mimeData is not None: self.actionPaste.setEnabled(mimeData.hasText()) self.actionPasteImage.setEnabled(mimeData.hasImage()) def insertFormatting(self, formatting): if formatting == 'table': dialog = InsertTableDialog(self) dialog.show() self.formattingBox.setCurrentIndex(0) return cursor = self.currentTab.editBox.textCursor() text = cursor.selectedText() moveCursorTo = None def c(cursor): nonlocal moveCursorTo moveCursorTo = cursor.position() def ensurenl(cursor): if not cursor.atBlockStart(): cursor.insertText('\n\n') toinsert = { 'header': (ensurenl, '# ', text), 'italic': ('*', text, c, '*'), 'bold': ('**', text, c, '**'), 'underline': ('<u>', text, c, '</u>'), 'numbering': (ensurenl, ' 1. ', text), 'bullets': (ensurenl, ' * ', text), 'image': ('![', text or self.tr('Alt text'), c, '](', self.tr('URL'), ')'), 'link': ('[', text or self.tr('Link text'), c, '](', self.tr('URL'), ')'), 'inline code': ('`', text, c, '`'), 'code block': (ensurenl, ' ', text), 'blockquote': (ensurenl, '> ', text), } if formatting not in toinsert: return cursor.beginEditBlock() for token in toinsert[formatting]: if callable(token): token(cursor) else: cursor.insertText(token) cursor.endEditBlock() self.formattingBox.setCurrentIndex(0) # Bring back the focus on the editor self.currentTab.editBox.setFocus(Qt.OtherFocusReason) if moveCursorTo: cursor.setPosition(moveCursorTo) self.currentTab.editBox.setTextCursor(cursor) def insertSymbol(self, num): if num: self.currentTab.editBox.insertPlainText('&'+self.usefulChars[num-1]+';') self.symbolBox.setCurrentIndex(0) def fileChanged(self, fileName): tab = None for testtab in self.iterateTabs(): if testtab.fileName == fileName: tab = testtab if tab is None: self.fileSystemWatcher.removePath(fileName) return if not QFile.exists(fileName): self.tabWidget.setCurrentWidget(tab) tab.editBox.document().setModified(True) QMessageBox.warning(self, '', self.tr( 'This file has been deleted by other application.\n' 'Please make sure you save the file before exit.')) elif not tab.editBox.document().isModified(): # File was not modified in ReText, reload silently tab.readTextFromFile() else: self.tabWidget.setCurrentWidget(tab) text = self.tr( 'This document has been modified by other application.\n' 'Do you want to reload the file (this will discard all ' 'your changes)?\n') if self.autoSaveEnabled: text += self.tr( 'If you choose to not reload the file, auto save mode will ' 'be disabled for this session to prevent data loss.') messageBox = QMessageBox(QMessageBox.Warning, '', text) reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole) messageBox.addButton(QMessageBox.Cancel) messageBox.exec() if messageBox.clickedButton() is reloadButton: tab.readTextFromFile() else: self.autoSaveEnabled = False tab.editBox.document().setModified(True) if fileName not in self.fileSystemWatcher.files(): # https://github.com/retext-project/retext/issues/137 self.fileSystemWatcher.addPath(fileName) def maybeSave(self, ind): tab = self.tabWidget.widget(ind) if self.autoSaveActive(tab): tab.saveTextToFile() return True if not tab.editBox.document().isModified(): return True self.tabWidget.setCurrentIndex(ind) ret = QMessageBox.warning(self, '', self.tr("The document has been modified.\nDo you want to save your changes?"), QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.saveFile(False) elif ret == QMessageBox.Cancel: return False return True def closeEvent(self, closeevent): for ind in range(self.tabWidget.count()): if not self.maybeSave(ind): return closeevent.ignore() if globalSettings.saveWindowGeometry: globalSettings.windowGeometry = self.saveGeometry() if globalSettings.openLastFilesOnStartup: files = [tab.fileName for tab in self.iterateTabs()] writeListToSettings("lastFileList", files) globalSettings.lastTabIndex = self.tabWidget.currentIndex() closeevent.accept() def viewHtml(self): htmlDlg = HtmlDialog(self) try: _, htmltext, _ = self.currentTab.getDocumentForExport(includeStyleSheet=False) except Exception: return self.printError() winTitle = self.currentTab.getBaseName() htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")") htmlDlg.textEdit.setPlainText(htmltext.rstrip()) htmlDlg.hl.rehighlight() htmlDlg.show() htmlDlg.raise_() htmlDlg.activateWindow() def insertImages(self): supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp'] fileFilter = ' (%s);;' % ' '.join('*' + ext for ext in supportedExtensions) fileNames, _selectedFilter = QFileDialog.getOpenFileNames(self, self.tr("Select one or several images to open"), QDir.currentPath(), self.tr("Supported files") + fileFilter + self.tr("All files (*)")) cursor = self.currentTab.editBox.textCursor() imagesMarkup = '\n'.join( self.currentTab.editBox.getImageMarkup(fileName) for fileName in fileNames) cursor.insertText(imagesMarkup) self.formattingBox.setCurrentIndex(0) self.currentTab.editBox.setFocus(Qt.OtherFocusReason) def openHelp(self): QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki')) def aboutDialog(self): QMessageBox.about(self, self.aboutWindowTitle, '<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__)) +'</b></p>' + self.tr('Simple but powerful editor' ' for Markdown and reStructuredText') +'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011–2020') +'<br><a href="https://github.com/retext-project/retext">'+self.tr('Website') +'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">' +self.tr('Markdown syntax') +'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">' +self.tr('reStructuredText syntax')+'</a></p>') def setDefaultMarkup(self, markupClass): globalSettings.defaultMarkup = markupClass.name for tab in self.iterateTabs(): if not tab.fileName: tab.updateActiveMarkupClass()
class ExperimentWindow(QMainWindow): experiment_finished = pyqtSignal([bool, bool]) MAX_GENERATIONS = 30 def __init__(self, mlc_local, experiment_name, experiment_closed_signal, parent=None): QMainWindow.__init__(self, parent) self._autogenerated_object = Ui_ExperimentWindow() self._autogenerated_object.setupUi(self) # Open the experiment self._mlc_local = mlc_local self._experiment_name = experiment_name self.setWindowTitle("Experiment {0}".format(self._experiment_name)) # Experiment tab parameters self._mlc_local.open_experiment(self._experiment_name) experiment_info = self._mlc_local.get_experiment_info(self._experiment_name) self._current_gen = int(experiment_info["generations"]) self._load_experiment_config() self._update_individuals_per_generation_list() self._update_experiment_info() self._update_individuals_figure() # Disable save_config_button until some change is made self._autogenerated_object.save_config_button.setDisabled(True) # Connect the function that updates the graphics of the Window when the # experiment evaluation finished self.experiment_finished.connect(self._update_experiment) # Signal to be emitted when the experiment is closed self._experiment_closed_signal = experiment_closed_signal # Watch changes in the experiment config file self._file_watcher = QFileSystemWatcher() conf_path_list = [self._mlc_local.get_working_dir(), self._experiment_name, self._experiment_name + ".conf"] # Use the splat operator to dearrange the list self._experiment_conf_path = os.path.join(*conf_path_list) self._file_watcher.addPath(self._experiment_conf_path) self._file_watcher.fileChanged.connect(self._reload_experiment_config) # Experiment in progress chart configuration self._chart_conf = ChartConfiguration(self._autogenerated_object) indivs_per_gen = Config.get_instance().getint("POPULATION", "size") self._chart_conf.init(indivs_per_gen) self._update_scatter_chart() # Manager of the first individuals self._first_indivs_manager = FirstIndividualsManager(parent=self, experiment_name=self._experiment_name, autogenerated_object=self._autogenerated_object, mlc_local=self._mlc_local) # Arduino board configurations self._board_config, self._serial_conn = mlc_local.get_board_configuration(self._experiment_name) ArduinoUserInterface.set_connection_builder(serial_connection_builder) # Update the arduino board group for the first case if self._autogenerated_object.disable_board_check.checkState() == Qt.Checked: self.on_disable_arduino_toggle(True) # FIXME: The code of show_convergence button is not implemented yet. Hide the button self._autogenerated_object.convergence_button.setVisible(False) def closeEvent(self, event): logger.debug('[EXPERIMENT {0}] [CLOSE_DIALOG] - Executing overriden closeEvent function' .format(self._experiment_name)) self._ask_if_experiment_config_must_be_saved() # Close the experiment self._file_watcher.removePath(self._experiment_conf_path) self._mlc_local.close_experiment(self._experiment_name) self._experiment_closed_signal.emit(self._experiment_name) def on_start_button_clicked(self): logger.debug('[EXPERIMENT {0}] [START_BUTTON] - Executing on_start_button_clicked function' .format(self._experiment_name)) try: ArduinoUserInterface.get_instance(protocol_config=self._board_config, conn_setup=self._serial_conn) except (ProtocolSetupException, ConnectionException) as err: logger.debug('[EXPERIMENT {0}] [BOARD_CONFIG] - ' 'Serial port could not be initialized. Error Msg: {1}' .format(self._experiment_name, err)) selection = QMessageBox.warning(self, "Connection failure", "The current connection arduino setup failed during initialization " "(Error: {0}), this may generate an error in your experiment.\n" "Do you want to continue?".format(err), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if selection == QMessageBox.No: logger.debug('[EXPERIMENT {0}] [START_BUTTON] - Experiment cancelled by user before start'.format( self._experiment_name)) QMessageBox.information(self, "Check setup", "Please, open the arduino board configurator in order to " "analize and resolve the error.", QMessageBox.Ok) return except Exception as err: self._report_arduino_unhandled_error(err) return from_gen = int(self._autogenerated_object.from_gen_combo.currentText()) to_gen = int(self._autogenerated_object.to_gen_combo.currentText()) number_of_gens = self._mlc_local.get_experiment_info(self._experiment_name)["generations"] gen_creator = None if number_of_gens == 0: from_gen = 0 # Hide the first individuals tab self._autogenerated_object.tabWidget.setTabEnabled(3, False) gen_creator = self._first_indivs_manager.get_gen_creator() # Check if the experiment range is ok if to_gen <= from_gen: logger.info("[EXPERIMENT {0}] [START_BUTTON] - Experiment can not be executed. " "'From Generation' index must be greater than 'To Generation' index " "From Gen: {1} - To Gen:{2}" .format(self._experiment_name, from_gen, to_gen)) QMessageBox.critical(self, "Invalid Generation Range", "Experiment can not be executed. 'From Generation' index " "must be greater than 'To Generation' index") return if from_gen != number_of_gens: # Check if the from generation value is not the last generation. If that's the case, show a warning # informing the user that generations will be lost if the experiment continues ret = QMessageBox.warning(self, "Generation To Be Lost Warning", "'From Generation' index is not the last generation. " "Generations will be lost if you continue the experiment. " "Do you want to continue?", QMessageBox.No | QMessageBox.Yes, QMessageBox.No) if ret == QMessageBox.No: return logger.info('[EXPERIMENT {0}] [START_BUTTON] - Proceed to execute experiment from Generation ' 'N°{1} to Generation N°{2}'.format(self._experiment_name, from_gen, to_gen)) progress_dialog = ExperimentInProgress(mlc_local=self._mlc_local, parent=self, experiment_name=self._experiment_name, to_gen=to_gen, from_gen=from_gen, chart_params=self._chart_conf.chart_params(), parent_signal=self.experiment_finished, gen_creator=gen_creator) progress_dialog.start() def on_prev_gen_button_clicked(self): logger.debug( '[EXPERIMENT {0}] [PREV_GEN_BUTTON] - Executing on_prev_gen_button_clicked function'.format(self._experiment_name)) experiment_info = self._mlc_local.get_experiment_info(self._experiment_name) number_of_gens = experiment_info["generations"] if self._current_gen > 1: self._current_gen -= 1 self._update_experiment_info() self._update_individuals_figure() self._update_scatter_chart() def on_next_gen_button_clicked(self): logger.debug( '[EXPERIMENT {0}] [NEXT_GEN_BUTTON] - Executing on_next_gen_button_clicked function'.format(self._experiment_name)) experiment_info = self._mlc_local.get_experiment_info(self._experiment_name) number_of_gens = experiment_info["generations"] if self._current_gen < number_of_gens: self._current_gen += 1 self._update_experiment_info() self._update_individuals_figure() self._update_scatter_chart() def on_test_button_clicked(self): logger.debug('[EXPERIMENT {0}] [TEST_BUTTON] - Executing on_test_button_clicked function' .format(self._experiment_name)) test_indiv_edit = self._autogenerated_object.test_indiv_edit if test_indiv_edit.text() == "": logger.warn('[EXPERIMENT {0}] [TEST_BUTTON] - The individual value cannot be an empty string' .format(self._experiment_name)) QMessageBox.information(self, "Test Evaluation Script", "The individual value cannot be an empty string.", QMessageBox.Ok) return cost = test_individual_value(parent=self, experiment_name=self._experiment_name, log_prefix="[EXPERIMENT_WINDOW]", indiv_value=test_indiv_edit.text(), config=Config.get_instance()) if cost != None: test_label_result = self._autogenerated_object.test_label_result test_label_result.setText(str(cost)) def on_test_preev_indiv_button_clicked(self): logger.debug('[EXPERIMENT {0}] [TEST_BUTTON] - Executing on_test_button_clicked function' .format(self._experiment_name)) test_indiv_edit = self._autogenerated_object.test_preev_indiv_edit if test_indiv_edit.text() == "": logger.warn('[EXPERIMENT {0}] [TEST_BUTTON] - The individual value cannot be an empty string' .format(self._experiment_name)) QMessageBox.information(self, "Test Preevaluation Script", "The individual value cannot be an empty string.", QMessageBox.Ok) return result = check_if_indiv_pass_preevaluation(parent=self, experiment_name=self._experiment_name, log_prefix="[EXPERIMENT_WINDOW]", indiv_value=test_indiv_edit.text(), config=Config.get_instance()) if result is not None: test_label_result = self._autogenerated_object.test_preev_indiv_result test_label_result.setText(str(result)) def on_log_check_clicked(self): logger.debug('[EXPERIMENT {0}] [LOG_CHECK_CLICKED] - Executing on_log_check_clicked function' .format(self._experiment_name)) self._update_individuals_figure() def on_show_all_check_clicked(self): logger.debug('[EXPERIMENT {0}] [SHOW_ALL_CHECK_CLICKED] - Executing on_show_all_check_clicked function' .format(self._experiment_name)) self._update_individuals_figure() def on_dimension_check_clicked(self): logger.debug('[EXPERIMENT {0}] [DIMENSION_CHECK_CLICKED] - Executing on_dimension_check_clicked function' .format(self._experiment_name)) # TODO: Don't know what the 3D graphic option should do yet... def on_save_config_button_clicked(self): logger.debug('[EXPERIMENT {0}] [SAVE_CONFIG_BUTTON_CLICKED] - Executing on_save_config_button_clicked function' .format(self._experiment_name)) # Remove the config file from the Qt File watcher momentarily # to avoid rendering unnecesary warnings self._file_watcher.removePath(self._experiment_conf_path) self._persist_experiment_config() self._file_watcher.addPath(self._experiment_conf_path) def on_tab_changed(self, tab_index): logger.debug('[EXPERIMENT {0}] [TAB_CHANGED] - Executing on_tab_changed function' .format(self._experiment_name)) self._ask_if_experiment_config_must_be_saved() def on_import_config_button_clicked(self): logger.debug('[EXPERIMENT {0}] [IMPORT_CONFIG_BUTTON_CLICKED] - Executing on_import_config_button_clicked function' .format(self._experiment_name)) # Get the path of the experiment to import config_file_path = QFileDialog.getOpenFileName(self, "Import Experiment", ".", "Config File (*.conf)")[0] if not config_file_path: # User clicked 'Cancel' or simply closed the Dialog return self._file_watcher.removePath(self._experiment_conf_path) self._mlc_local.set_experiment_configuration_from_file(self._experiment_name, config_file_path) self._load_experiment_config() QMessageBox.information(self, "Config File Imported", "New config file {0} was succesfully imported".format(config_file_path)) logger.info("[EXPERIMENT {0}] [EXPORT_CONFIG] - New config file {1} was " "succesfully imported.".format(self._experiment_name, config_file_path)) self._autogenerated_object.save_config_button.setDisabled(True) self._file_watcher.addPath(self._experiment_conf_path) def on_export_config_button_clicked(self): logger.debug('[EXPERIMENT {0}] [EXPORT_CONFIG_BUTTON_CLICKED] - Executing on_export_config_button_clicked function' .format(self._experiment_name)) export_dir = QFileDialog.getExistingDirectory(self, 'Choose the directory where to export the configuration file', '.', QFileDialog.ShowDirsOnly) if not export_dir: # User clicked 'Cancel' or simply closed the Dialog return export_file_name = os.path.join(export_dir, self._experiment_name + ".conf") try: shutil.copyfile(self._experiment_conf_path, export_file_name) except Exception, err: QMessageBox.critical(self, "Config File Not Exported", "Config File could not be exported. " "Error {0}".format(err)) logger.error("[EXPERIMENT {0}] [EXPORT_CONFIG] - Config file could not " "be exported. Error: {0}".format(err)) return logger.info("[EXPERIMENT {0}] [EXPORT_CONFIG] - Config file was " "succesfully exported. Location: {1}" .format(self._experiment_name, export_file_name)) QMessageBox.information(self, "Config File Exported", "Config File was succesfully exported. You can find it at {0}" .format(export_file_name))
class ReTextWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.resize(950, 700) screenRect = QDesktopWidget().screenGeometry() if globalSettings.windowGeometry: self.restoreGeometry(globalSettings.windowGeometry) else: self.move((screenRect.width()-self.width())/2, (screenRect.height()-self.height())/2) if not screenRect.contains(self.geometry()): self.showMaximized() if globalSettings.iconTheme: QIcon.setThemeName(globalSettings.iconTheme) if QIcon.themeName() in ('hicolor', ''): if not QFile.exists(icon_path + 'document-new.png'): QIcon.setThemeName(get_icon_theme()) if QFile.exists(icon_path+'retext.png'): self.setWindowIcon(QIcon(icon_path+'retext.png')) elif QFile.exists('/usr/share/pixmaps/retext.png'): self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png')) else: self.setWindowIcon(QIcon.fromTheme('retext', QIcon.fromTheme('accessories-text-editor'))) self.tabWidget = QTabWidget(self) self.initTabWidget() self.setCentralWidget(self.tabWidget) self.tabWidget.currentChanged.connect(self.changeIndex) self.tabWidget.tabCloseRequested.connect(self.closeTab) toolBar = QToolBar(self.tr('File toolbar'), self) self.addToolBar(Qt.TopToolBarArea, toolBar) self.editBar = QToolBar(self.tr('Edit toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.editBar) self.searchBar = QToolBar(self.tr('Search toolbar'), self) self.addToolBar(Qt.BottomToolBarArea, self.searchBar) toolBar.setVisible(not globalSettings.hideToolBar) self.editBar.setVisible(not globalSettings.hideToolBar) self.actionNew = self.act(self.tr('New'), 'document-new', self.createNew, shct=QKeySequence.New) self.actionNew.setPriority(QAction.LowPriority) self.actionOpen = self.act(self.tr('Open'), 'document-open', self.openFile, shct=QKeySequence.Open) self.actionOpen.setPriority(QAction.LowPriority) self.actionSetEncoding = self.act(self.tr('Set encoding'), trig=self.showEncodingDialog) self.actionSetEncoding.setEnabled(False) self.actionReload = self.act(self.tr('Reload'), 'view-refresh', lambda: self.currentTab.readTextFromFile()) self.actionReload.setEnabled(False) self.actionSave = self.act(self.tr('Save'), 'document-save', self.saveFile, shct=QKeySequence.Save) self.actionSave.setEnabled(False) self.actionSave.setPriority(QAction.LowPriority) self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as', self.saveFileAs, shct=QKeySequence.SaveAs) self.actionNextTab = self.act(self.tr('Next tab'), 'go-next', lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown) self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous', lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp) self.actionPrint = self.act(self.tr('Print'), 'document-print', self.printFile, shct=QKeySequence.Print) self.actionPrint.setPriority(QAction.LowPriority) self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview', self.printPreview) self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml) self.actionChangeEditorFont = self.act(self.tr('Change editor font'), trig=self.changeEditorFont) self.actionChangePreviewFont = self.act(self.tr('Change preview font'), trig=self.changePreviewFont) self.actionSearch = self.act(self.tr('Find text'), 'edit-find', shct=QKeySequence.Find) self.actionSearch.setCheckable(True) self.actionSearch.triggered[bool].connect(self.searchBar.setVisible) self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged) self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E, trigbool=self.preview) if QIcon.hasThemeIcon('document-preview'): self.actionPreview.setIcon(QIcon.fromTheme('document-preview')) elif QIcon.hasThemeIcon('preview-file'): self.actionPreview.setIcon(QIcon.fromTheme('preview-file')) elif QIcon.hasThemeIcon('x-office-document'): self.actionPreview.setIcon(QIcon.fromTheme('x-office-document')) else: self.actionPreview.setIcon(QIcon(icon_path+'document-preview.png')) self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L, trigbool=self.enableLivePreview) menuPreview = QMenu() menuPreview.addAction(self.actionLivePreview) self.actionPreview.setMenu(menuPreview) self.actionTableMode = self.act(self.tr('Table mode'), shct=Qt.CTRL+Qt.Key_T, trigbool=lambda x: self.currentTab.editBox.enableTableMode(x)) if ReTextFakeVimHandler: self.actionFakeVimMode = self.act(self.tr('FakeVim mode'), shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode) if globalSettings.useFakeVim: self.actionFakeVimMode.setChecked(True) self.enableFakeVimMode(True) self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen', shct=Qt.Key_F11, trigbool=self.enableFullScreen) self.actionFullScreen.setPriority(QAction.LowPriority) self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system', trig=self.openConfigDialog) self.actionConfig.setMenuRole(QAction.PreferencesRole) self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml) self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf) self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf) self.getExportExtensionsList() self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit) self.actionQuit.setMenuRole(QAction.QuitRole) self.actionQuit.triggered.connect(self.close) self.actionUndo = self.act(self.tr('Undo'), 'edit-undo', lambda: self.currentTab.editBox.undo(), shct=QKeySequence.Undo) self.actionRedo = self.act(self.tr('Redo'), 'edit-redo', lambda: self.currentTab.editBox.redo(), shct=QKeySequence.Redo) self.actionCopy = self.act(self.tr('Copy'), 'edit-copy', lambda: self.currentTab.editBox.copy(), shct=QKeySequence.Copy) self.actionCut = self.act(self.tr('Cut'), 'edit-cut', lambda: self.currentTab.editBox.cut(), shct=QKeySequence.Cut) self.actionPaste = self.act(self.tr('Paste'), 'edit-paste', lambda: self.currentTab.editBox.paste(), shct=QKeySequence.Paste) self.actionUndo.setEnabled(False) self.actionRedo.setEnabled(False) self.actionCopy.setEnabled(False) self.actionCut.setEnabled(False) qApp = QApplication.instance() qApp.clipboard().dataChanged.connect(self.clipboardDataChanged) self.clipboardDataChanged() if enchant_available: self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck) self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale) self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit) self.actionWebKit.setChecked(globalSettings.useWebKit) self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir) self.actionFind = self.act(self.tr('Next'), 'go-next', self.find, shct=QKeySequence.FindNext) self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous', lambda: self.find(back=True), shct=QKeySequence.FindPrevious) self.actionCloseSearch = self.act(self.tr('Close'), 'window-close', lambda: self.searchBar.setVisible(False)) self.actionCloseSearch.setPriority(QAction.LowPriority) self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp) self.aboutWindowTitle = self.tr('About ReText') self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog) self.actionAbout.setMenuRole(QAction.AboutRole) self.actionAboutQt = self.act(self.tr('About Qt')) self.actionAboutQt.setMenuRole(QAction.AboutQtRole) self.actionAboutQt.triggered.connect(qApp.aboutQt) availableMarkups = markups.get_available_markups() if not availableMarkups: print('Warning: no markups are available!') self.defaultMarkup = availableMarkups[0] if availableMarkups else None if globalSettings.defaultMarkup: mc = markups.find_markup_class_by_name(globalSettings.defaultMarkup) if mc and mc.available(): self.defaultMarkup = mc if len(availableMarkups) > 1: self.chooseGroup = QActionGroup(self) markupActions = [] for markup in availableMarkups: markupAction = self.act(markup.name, trigbool=self.markupFunction(markup)) if markup == self.defaultMarkup: markupAction.setChecked(True) self.chooseGroup.addAction(markupAction) markupActions.append(markupAction) self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold, trig=lambda: self.insertFormatting('bold')) self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic, trig=lambda: self.insertFormatting('italic')) self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline, trig=lambda: self.insertFormatting('underline')) self.usefulTags = ('header', 'italic', 'bold', 'underline', 'numbering', 'bullets', 'image', 'link', 'inline code', 'code block', 'blockquote') self.usefulChars = ('deg', 'divide', 'dollar', 'hellip', 'laquo', 'larr', 'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo', 'rarr', 'rsquo', 'times') self.formattingBox = QComboBox(self.editBar) self.formattingBox.addItem(self.tr('Formatting')) self.formattingBox.addItems(self.usefulTags) self.formattingBox.activated[str].connect(self.insertFormatting) self.symbolBox = QComboBox(self.editBar) self.symbolBox.addItem(self.tr('Symbols')) self.symbolBox.addItems(self.usefulChars) self.symbolBox.activated.connect(self.insertSymbol) self.updateStyleSheet() menubar = self.menuBar() menuFile = menubar.addMenu(self.tr('File')) menuEdit = menubar.addMenu(self.tr('Edit')) menuHelp = menubar.addMenu(self.tr('Help')) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent')) self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles) menuFile.addAction(self.actionShow) menuFile.addAction(self.actionSetEncoding) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addSeparator() menuFile.addAction(self.actionNextTab) menuFile.addAction(self.actionPrevTab) menuFile.addSeparator() menuExport = menuFile.addMenu(self.tr('Export')) menuExport.addAction(self.actionSaveHtml) menuExport.addAction(self.actionOdf) menuExport.addAction(self.actionPdf) if self.extensionActions: menuExport.addSeparator() for action, mimetype in self.extensionActions: menuExport.addAction(action) menuExport.aboutToShow.connect(self.updateExtensionsVisibility) menuFile.addAction(self.actionPrint) menuFile.addAction(self.actionPrintPreview) menuFile.addSeparator() menuFile.addAction(self.actionQuit) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addSeparator() if enchant_available: menuSC = menuEdit.addMenu(self.tr('Spell check')) menuSC.addAction(self.actionEnableSC) menuSC.addAction(self.actionSetLocale) menuEdit.addAction(self.actionSearch) menuEdit.addAction(self.actionChangeEditorFont) menuEdit.addAction(self.actionChangePreviewFont) menuEdit.addSeparator() if len(availableMarkups) > 1: self.menuMode = menuEdit.addMenu(self.tr('Default markup')) for markupAction in markupActions: self.menuMode.addAction(markupAction) menuFormat = menuEdit.addMenu(self.tr('Formatting')) menuFormat.addAction(self.actionBold) menuFormat.addAction(self.actionItalic) menuFormat.addAction(self.actionUnderline) menuEdit.addAction(self.actionWebKit) menuEdit.addSeparator() menuEdit.addAction(self.actionViewHtml) menuEdit.addAction(self.actionPreview) menuEdit.addAction(self.actionTableMode) if ReTextFakeVimHandler: menuEdit.addAction(self.actionFakeVimMode) menuEdit.addSeparator() menuEdit.addAction(self.actionFullScreen) menuEdit.addAction(self.actionConfig) menuHelp.addAction(self.actionHelp) menuHelp.addSeparator() menuHelp.addAction(self.actionAbout) menuHelp.addAction(self.actionAboutQt) toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) toolBar.addAction(self.actionNew) toolBar.addSeparator() toolBar.addAction(self.actionOpen) toolBar.addAction(self.actionSave) toolBar.addAction(self.actionPrint) toolBar.addSeparator() toolBar.addAction(self.actionPreview) toolBar.addAction(self.actionFullScreen) self.editBar.addAction(self.actionUndo) self.editBar.addAction(self.actionRedo) self.editBar.addSeparator() self.editBar.addAction(self.actionCut) self.editBar.addAction(self.actionCopy) self.editBar.addAction(self.actionPaste) self.editBar.addSeparator() self.editBar.addWidget(self.formattingBox) self.editBar.addWidget(self.symbolBox) self.searchEdit = QLineEdit(self.searchBar) self.searchEdit.setPlaceholderText(self.tr('Search')) self.searchEdit.returnPressed.connect(self.find) self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar) self.searchBar.addWidget(self.searchEdit) self.searchBar.addSeparator() self.searchBar.addWidget(self.csBox) self.searchBar.addAction(self.actionFindPrev) self.searchBar.addAction(self.actionFind) self.searchBar.addAction(self.actionCloseSearch) self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.searchBar.setVisible(False) self.autoSaveEnabled = globalSettings.autoSave if self.autoSaveEnabled: timer = QTimer(self) timer.start(60000) timer.timeout.connect(self.saveAll) self.ind = None if enchant_available: self.sl = globalSettings.spellCheckLocale if self.sl: try: enchant.Dict(self.sl) except Exception as e: print(e, file=sys.stderr) self.sl = None if globalSettings.spellCheck: self.actionEnableSC.setChecked(True) self.fileSystemWatcher = QFileSystemWatcher() self.fileSystemWatcher.fileChanged.connect(self.fileChanged) def iterateTabs(self): for i in range(self.tabWidget.count()): yield self.tabWidget.widget(i).tab def updateStyleSheet(self): if globalSettings.styleSheet: sheetfile = QFile(globalSettings.styleSheet) sheetfile.open(QIODevice.ReadOnly) self.ss = QTextStream(sheetfile).readAll() sheetfile.close() else: palette = QApplication.palette() self.ss = 'html { color: %s; }\n' % palette.color(QPalette.WindowText).name() self.ss += 'td, th { border: 1px solid #c3c3c3; padding: 0 3px 0 3px; }\n' self.ss += 'table { border-collapse: collapse; }\n' def initTabWidget(self): def dragEnterEvent(e): e.acceptProposedAction() def dropEvent(e): fn = bytes(e.mimeData().data('text/plain')).decode().rstrip() if fn.startswith('file:'): fn = QUrl(fn).toLocalFile() self.openFileWrapper(fn) self.tabWidget.setTabsClosable(True) self.tabWidget.setAcceptDrops(True) self.tabWidget.setMovable(True) self.tabWidget.dragEnterEvent = dragEnterEvent self.tabWidget.dropEvent = dropEvent def act(self, name, icon=None, trig=None, trigbool=None, shct=None): if not isinstance(shct, QKeySequence): shct = QKeySequence(shct) if icon: action = QAction(self.actIcon(icon), name, self) else: action = QAction(name, self) if trig: action.triggered.connect(trig) elif trigbool: action.setCheckable(True) action.triggered[bool].connect(trigbool) if shct: action.setShortcut(shct) return action def actIcon(self, name): return QIcon.fromTheme(name, QIcon(icon_path+name+'.png')) def printError(self): import traceback print('Exception occured while parsing document:', file=sys.stderr) traceback.print_exc() def createTab(self, fileName): self.currentTab = ReTextTab(self, fileName, previewState=int(globalSettings.livePreviewByDefault)) self.tabWidget.addTab(self.currentTab.getSplitter(), self.tr("New document")) def closeTab(self, ind): if self.maybeSave(ind): if self.tabWidget.count() == 1: self.createTab("") currentWidget = self.tabWidget.widget(ind) if currentWidget.tab.fileName: self.fileSystemWatcher.removePath(currentWidget.tab.fileName) del currentWidget.tab self.tabWidget.removeTab(ind) def docTypeChanged(self): markupClass = self.currentTab.getMarkupClass() if type(self.currentTab.markup) != markupClass: self.currentTab.setMarkupClass(markupClass) self.currentTab.updatePreviewBox() dtMarkdown = (markupClass == markups.MarkdownMarkup) dtMkdOrReST = dtMarkdown or (markupClass == markups.ReStructuredTextMarkup) self.formattingBox.setEnabled(dtMarkdown) self.symbolBox.setEnabled(dtMarkdown) self.actionUnderline.setEnabled(dtMarkdown) self.actionBold.setEnabled(dtMkdOrReST) self.actionItalic.setEnabled(dtMkdOrReST) canReload = bool(self.currentTab.fileName) and not self.autoSaveActive() self.actionSetEncoding.setEnabled(canReload) self.actionReload.setEnabled(canReload) def changeIndex(self, ind): self.currentTab = self.tabWidget.currentWidget().tab editBox = self.currentTab.editBox previewState = self.currentTab.previewState self.actionUndo.setEnabled(editBox.document().isUndoAvailable()) self.actionRedo.setEnabled(editBox.document().isRedoAvailable()) self.actionCopy.setEnabled(editBox.textCursor().hasSelection()) self.actionCut.setEnabled(editBox.textCursor().hasSelection()) self.actionPreview.setChecked(previewState >= PreviewLive) self.actionLivePreview.setChecked(previewState == PreviewLive) self.actionTableMode.setChecked(editBox.tableModeEnabled) self.editBar.setEnabled(previewState < PreviewNormal) self.ind = ind if self.currentTab.fileName: self.setCurrentFile() else: self.setWindowTitle(self.tr('New document') + '[*]') self.docTypeChanged() self.modificationChanged(editBox.document().isModified()) editBox.setFocus(Qt.OtherFocusReason) def changeEditorFont(self): font, ok = QFontDialog.getFont(globalSettings.editorFont, self) if ok: globalSettings.editorFont = font for tab in self.iterateTabs(): tab.editBox.updateFont() def changePreviewFont(self): font, ok = QFontDialog.getFont(globalSettings.font, self) if ok: globalSettings.font = font for tab in self.iterateTabs(): tab.updatePreviewBox() def preview(self, viewmode): self.currentTab.previewState = viewmode * 2 self.actionLivePreview.setChecked(False) self.editBar.setDisabled(viewmode) self.currentTab.updateBoxesVisibility() if viewmode: self.currentTab.updatePreviewBox() def enableLivePreview(self, livemode): self.currentTab.previewState = int(livemode) self.actionPreview.setChecked(livemode) self.editBar.setEnabled(True) self.currentTab.updateBoxesVisibility() if livemode: self.currentTab.updatePreviewBox() def enableWebKit(self, enable): globalSettings.useWebKit = enable for i in range(self.tabWidget.count()): splitter = self.tabWidget.widget(i) tab = splitter.tab tab.previewBox.disconnectExternalSignals() tab.previewBox.setParent(None) tab.previewBox.deleteLater() tab.previewBox = tab.createPreviewBox(tab.editBox) tab.previewBox.setMinimumWidth(125) splitter.addWidget(tab.previewBox) splitter.setSizes((50, 50)) tab.updatePreviewBox() tab.updateBoxesVisibility() def enableCopy(self, copymode): self.actionCopy.setEnabled(copymode) self.actionCut.setEnabled(copymode) def enableFullScreen(self, yes): if yes: self.showFullScreen() else: self.showNormal() def openConfigDialog(self): dlg = ConfigDialog(self) dlg.setWindowTitle(self.tr('Preferences')) dlg.show() def enableFakeVimMode(self, yes): globalSettings.useFakeVim = yes if yes: FakeVimMode.init(self) for tab in self.iterateTabs(): tab.installFakeVimHandler() else: FakeVimMode.exit(self) def enableSpellCheck(self, yes): if yes: self.setAllDictionaries(enchant.Dict(self.sl or None)) else: self.setAllDictionaries(None) globalSettings.spellCheck = yes def setAllDictionaries(self, dictionary): for tab in self.iterateTabs(): hl = tab.highlighter hl.dictionary = dictionary hl.rehighlight() def changeLocale(self): if self.sl: localedlg = LocaleDialog(self, defaultText=self.sl) else: localedlg = LocaleDialog(self) if localedlg.exec() != QDialog.Accepted: return sl = localedlg.localeEdit.text() setdefault = localedlg.checkBox.isChecked() if sl: try: sl = str(sl) enchant.Dict(sl) except Exception as e: QMessageBox.warning(self, '', str(e)) else: self.sl = sl self.enableSpellCheck(self.actionEnableSC.isChecked()) else: self.sl = None self.enableSpellCheck(self.actionEnableSC.isChecked()) if setdefault: globalSettings.spellCheckLocale = sl def searchBarVisibilityChanged(self, visible): self.actionSearch.setChecked(visible) if visible: self.searchEdit.setFocus(Qt.ShortcutFocusReason) def find(self, back=False): flags = QTextDocument.FindFlags() if back: flags |= QTextDocument.FindBackward if self.csBox.isChecked(): flags |= QTextDocument.FindCaseSensitively text = self.searchEdit.text() editBox = self.currentTab.editBox cursor = editBox.textCursor() newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) cursor.movePosition(QTextCursor.End if back else QTextCursor.Start) newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) self.setSearchEditColor(False) def setSearchEditColor(self, found): palette = self.searchEdit.palette() palette.setColor(QPalette.Active, QPalette.Base, Qt.white if found else QColor(255, 102, 102)) self.searchEdit.setPalette(palette) def showInDir(self): if self.currentTab.fileName: path = QFileInfo(self.currentTab.fileName).path() QDesktopServices.openUrl(QUrl.fromLocalFile(path)) else: QMessageBox.warning(self, '', self.tr("Please, save the file somewhere.")) def setCurrentFile(self): self.setWindowTitle("") self.tabWidget.setTabText(self.ind, self.currentTab.getDocumentTitle(baseName=True)) self.tabWidget.setTabToolTip(self.ind, self.currentTab.fileName or '') self.setWindowFilePath(self.currentTab.fileName) files = readListFromSettings("recentFileList") while self.currentTab.fileName in files: files.remove(self.currentTab.fileName) files.insert(0, self.currentTab.fileName) if len(files) > 10: del files[10:] writeListToSettings("recentFileList", files) QDir.setCurrent(QFileInfo(self.currentTab.fileName).dir().path()) self.docTypeChanged() def createNew(self, text=None): self.createTab("") self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if text: self.currentTab.editBox.textCursor().insertText(text) def switchTab(self, shift=1): self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count()) def updateRecentFiles(self): self.menuRecentFiles.clear() self.recentFilesActions = [] filesOld = readListFromSettings("recentFileList") files = [] for f in filesOld: if QFile.exists(f): files.append(f) self.recentFilesActions.append(self.act(f, trig=self.openFunction(f))) writeListToSettings("recentFileList", files) for action in self.recentFilesActions: self.menuRecentFiles.addAction(action) def markupFunction(self, markup): return lambda: self.setDefaultMarkup(markup) def openFunction(self, fileName): return lambda: self.openFileWrapper(fileName) def extensionFunction(self, data): return lambda: \ self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension']) def getExportExtensionsList(self): extensions = [] for extsprefix in datadirs: extsdir = QDir(extsprefix+'/export-extensions/') if extsdir.exists(): for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], QDir.Files | QDir.Readable): extensions.append(self.readExtension(fileInfo.filePath())) locale = QLocale.system().name() self.extensionActions = [] for extension in extensions: try: if ('Name[%s]' % locale) in extension: name = extension['Name[%s]' % locale] elif ('Name[%s]' % locale.split('_')[0]) in extension: name = extension['Name[%s]' % locale.split('_')[0]] else: name = extension['Name'] data = {} for prop in ('FileFilter', 'DefaultExtension', 'Exec'): if 'X-ReText-'+prop in extension: data[prop] = extension['X-ReText-'+prop] elif prop in extension: data[prop] = extension[prop] else: data[prop] = '' action = self.act(name, trig=self.extensionFunction(data)) if 'Icon' in extension: action.setIcon(self.actIcon(extension['Icon'])) mimetype = extension['MimeType'] if 'MimeType' in extension else None except KeyError: print('Failed to parse extension: Name is required', file=sys.stderr) else: self.extensionActions.append((action, mimetype)) def updateExtensionsVisibility(self): markupClass = self.currentTab.getMarkupClass() for action in self.extensionActions: if markupClass is None: action[0].setEnabled(False) continue mimetype = action[1] if mimetype == None: enabled = True elif markupClass == markups.MarkdownMarkup: enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown")) elif markupClass == markups.ReStructuredTextMarkup: enabled = (mimetype in ("text/x-retext-rst", "text/x-rst")) else: enabled = False action[0].setEnabled(enabled) def readExtension(self, fileName): extFile = QFile(fileName) extFile.open(QIODevice.ReadOnly) extension = {} stream = QTextStream(extFile) while not stream.atEnd(): line = stream.readLine() if '=' in line: index = line.index('=') extension[line[:index].rstrip()] = line[index+1:].lstrip() extFile.close() return extension def openFile(self): supportedExtensions = ['.txt'] for markup in markups.get_all_markups(): supportedExtensions += markup.file_extensions fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;' fileNames = QFileDialog.getOpenFileNames(self, self.tr("Select one or several files to open"), "", self.tr("Supported files") + fileFilter + self.tr("All files (*)")) for fileName in fileNames[0]: self.openFileWrapper(fileName) def openFileWrapper(self, fileName): if not fileName: return fileName = QFileInfo(fileName).canonicalFilePath() exists = False for i, tab in enumerate(self.iterateTabs()): if tab.fileName == fileName: exists = True ex = i if exists: self.tabWidget.setCurrentIndex(ex) elif QFile.exists(fileName): noEmptyTab = ( (self.ind is None) or self.currentTab.fileName or self.currentTab.editBox.toPlainText() or self.currentTab.editBox.document().isModified() ) if noEmptyTab: self.createTab(fileName) self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if fileName: self.fileSystemWatcher.addPath(fileName) self.currentTab.fileName = fileName self.currentTab.readTextFromFile() editBox = self.currentTab.editBox self.setCurrentFile() self.setWindowModified(editBox.document().isModified()) def showEncodingDialog(self): if not self.maybeSave(self.ind): return encoding, ok = QInputDialog.getItem(self, '', self.tr('Select file encoding from the list:'), [bytes(b).decode() for b in QTextCodec.availableCodecs()], 0, False) if ok: self.currentTab.readTextFromFile(encoding) def saveFile(self): self.saveFileMain(dlg=False) def saveFileAs(self): self.saveFileMain(dlg=True) def saveAll(self): for tab in self.iterateTabs(): if tab.fileName and QFileInfo(tab.fileName).isWritable(): tab.saveTextToFile() tab.editBox.document().setModified(False) def saveFileMain(self, dlg): if (not self.currentTab.fileName) or dlg: markupClass = self.currentTab.getMarkupClass() if (markupClass is None) or not hasattr(markupClass, 'default_extension'): defaultExt = self.tr("Plain text (*.txt)") ext = ".txt" else: defaultExt = self.tr('%s files', 'Example of final string: Markdown files') \ % markupClass.name + ' (' + str.join(' ', ('*'+extension for extension in markupClass.file_extensions)) + ')' if markupClass == markups.MarkdownMarkup: ext = globalSettings.markdownDefaultFileExtension elif markupClass == markups.ReStructuredTextMarkup: ext = globalSettings.restDefaultFileExtension else: ext = markupClass.default_extension newFileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", defaultExt)[0] if newFileName: if not QFileInfo(newFileName).suffix(): newFileName += ext if self.currentTab.fileName: self.fileSystemWatcher.removePath(self.currentTab.fileName) self.currentTab.fileName = newFileName self.actionSetEncoding.setDisabled(self.autoSaveActive()) if self.currentTab.fileName: if self.currentTab.saveTextToFile(): self.setCurrentFile() self.currentTab.editBox.document().setModified(False) self.setWindowModified(False) return True else: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return False def saveHtml(self, fileName): if not QFileInfo(fileName).suffix(): fileName += ".html" try: htmltext = self.currentTab.getHtml(includeStyleSheet=False, webenv=True) except Exception: return self.printError() htmlFile = QFile(fileName) htmlFile.open(QIODevice.WriteOnly) html = QTextStream(htmlFile) if globalSettings.defaultCodec: html.setCodec(globalSettings.defaultCodec) html << htmltext htmlFile.close() def textDocument(self): td = QTextDocument() td.setMetaInformation(QTextDocument.DocumentTitle, self.currentTab.getDocumentTitle()) if self.ss: td.setDefaultStyleSheet(self.ss) td.setHtml(self.currentTab.getHtml()) td.setDefaultFont(globalSettings.font) return td def saveOdf(self): try: document = self.textDocument() except Exception: return self.printError() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to ODT"), "", self.tr("OpenDocument text files (*.odt)"))[0] if not QFileInfo(fileName).suffix(): fileName += ".odt" writer = QTextDocumentWriter(fileName) writer.setFormat(b"odf") writer.write(document) def saveFileHtml(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", self.tr("HTML files (*.html *.htm)"))[0] if fileName: self.saveHtml(fileName) def getDocumentForPrint(self): if globalSettings.useWebKit: return self.currentTab.previewBox try: return self.textDocument() except Exception: self.printError() def standardPrinter(self): printer = QPrinter(QPrinter.HighResolution) printer.setDocName(self.currentTab.getDocumentTitle()) printer.setCreator('ReText %s' % app_version) return printer def savePdf(self): self.currentTab.updatePreviewBox() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to PDF"), "", self.tr("PDF files (*.pdf)"))[0] if fileName: if not QFileInfo(fileName).suffix(): fileName += ".pdf" printer = self.standardPrinter() printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fileName) document = self.getDocumentForPrint() if document != None: document.print(printer) def printFile(self): self.currentTab.updatePreviewBox() printer = self.standardPrinter() dlg = QPrintDialog(printer, self) dlg.setWindowTitle(self.tr("Print document")) if (dlg.exec() == QDialog.Accepted): document = self.getDocumentForPrint() if document != None: document.print(printer) def printPreview(self): document = self.getDocumentForPrint() if document == None: return printer = self.standardPrinter() preview = QPrintPreviewDialog(printer, self) preview.paintRequested.connect(document.print) preview.exec() def runExtensionCommand(self, command, filefilter, defaultext): of = ('%of' in command) html = ('%html' in command) if of: if defaultext and not filefilter: filefilter = '*'+defaultext fileName = QFileDialog.getSaveFileName(self, self.tr('Export document'), '', filefilter)[0] if not fileName: return if defaultext and not QFileInfo(fileName).suffix(): fileName += defaultext basename = '.%s.retext-temp' % self.currentTab.getDocumentTitle(baseName=True) if html: tmpname = basename+'.html' self.saveHtml(tmpname) else: tmpname = basename + self.currentTab.getMarkupClass().default_extension self.currentTab.saveTextToFile(fileName=tmpname, addToWatcher=False) command = command.replace('%of', '"out'+defaultext+'"') command = command.replace('%html' if html else '%if', '"'+tmpname+'"') try: Popen(str(command), shell=True).wait() except Exception as error: errorstr = str(error) QMessageBox.warning(self, '', self.tr('Failed to execute the command:') + '\n' + errorstr) QFile(tmpname).remove() if of: QFile('out'+defaultext).rename(fileName) def autoSaveActive(self, ind=None): tab = self.currentTab if ind is None else self.tabWidget.widget(ind).tab return (self.autoSaveEnabled and tab.fileName and QFileInfo(tab.fileName).isWritable()) def modificationChanged(self, changed): if self.autoSaveActive(): changed = False self.actionSave.setEnabled(changed) self.setWindowModified(changed) def clipboardDataChanged(self): mimeData = QApplication.instance().clipboard().mimeData() if mimeData is not None: self.actionPaste.setEnabled(mimeData.hasText()) def insertFormatting(self, formatting): cursor = self.currentTab.editBox.textCursor() text = cursor.selectedText() moveCursorTo = None def c(cursor): nonlocal moveCursorTo moveCursorTo = cursor.position() def ensurenl(cursor): if not cursor.atBlockStart(): cursor.insertText('\n\n') toinsert = { 'header': (ensurenl, '# ', text), 'italic': ('*', text, c, '*'), 'bold': ('**', text, c, '**'), 'underline': ('<u>', text, c, '</u>'), 'numbering': (ensurenl, ' 1. ', text), 'bullets': (ensurenl, ' * ', text), 'image': ('![', text or self.tr('Alt text'), c, '](', self.tr('URL'), ')'), 'link': ('[', text or self.tr('Link text'), c, '](', self.tr('URL'), ')'), 'inline code': ('`', text, c, '`'), 'code block': (ensurenl, ' ', text), 'blockquote': (ensurenl, '> ', text), } if formatting not in toinsert: return cursor.beginEditBlock() for token in toinsert[formatting]: if callable(token): token(cursor) else: cursor.insertText(token) cursor.endEditBlock() self.formattingBox.setCurrentIndex(0) # Bring back the focus on the editor self.currentTab.editBox.setFocus(Qt.OtherFocusReason) if moveCursorTo: cursor.setPosition(moveCursorTo) self.currentTab.editBox.setTextCursor(cursor) def insertSymbol(self, num): if num: self.currentTab.editBox.insertPlainText('&'+self.usefulChars[num-1]+';') self.symbolBox.setCurrentIndex(0) def fileChanged(self, fileName): ind = None for testind, tab in enumerate(self.iterateTabs()): if tab.fileName == fileName: ind = testind if ind is None: self.fileSystemWatcher.removePath(fileName) self.tabWidget.setCurrentIndex(ind) if not QFile.exists(fileName): self.currentTab.editBox.document().setModified(True) QMessageBox.warning(self, '', self.tr( 'This file has been deleted by other application.\n' 'Please make sure you save the file before exit.')) elif not self.currentTab.editBox.document().isModified(): # File was not modified in ReText, reload silently self.currentTab.readTextFromFile() self.currentTab.updatePreviewBox() else: text = self.tr( 'This document has been modified by other application.\n' 'Do you want to reload the file (this will discard all ' 'your changes)?\n') if self.autoSaveEnabled: text += self.tr( 'If you choose to not reload the file, auto save mode will ' 'be disabled for this session to prevent data loss.') messageBox = QMessageBox(QMessageBox.Warning, '', text) reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole) messageBox.addButton(QMessageBox.Cancel) messageBox.exec() if messageBox.clickedButton() is reloadButton: self.currentTab.readTextFromFile() self.currentTab.updatePreviewBox() else: self.autoSaveEnabled = False self.currentTab.editBox.document().setModified(True) if fileName not in self.fileSystemWatcher.files(): # https://github.com/retext-project/retext/issues/137 self.fileSystemWatcher.addPath(fileName) def maybeSave(self, ind): tab = self.tabWidget.widget(ind).tab if self.autoSaveActive(ind): tab.saveTextToFile() return True if not tab.editBox.document().isModified(): return True self.tabWidget.setCurrentIndex(ind) ret = QMessageBox.warning(self, '', self.tr("The document has been modified.\nDo you want to save your changes?"), QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.saveFileMain(False) elif ret == QMessageBox.Cancel: return False return True def closeEvent(self, closeevent): for ind in range(self.tabWidget.count()): if not self.maybeSave(ind): return closeevent.ignore() if globalSettings.saveWindowGeometry and not self.isMaximized(): globalSettings.windowGeometry = self.saveGeometry() closeevent.accept() def viewHtml(self): htmlDlg = HtmlDialog(self) try: htmltext = self.currentTab.getHtml(includeStyleSheet=False) except Exception: return self.printError() winTitle = self.currentTab.getDocumentTitle(baseName=True) htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")") htmlDlg.textEdit.setPlainText(htmltext.rstrip()) htmlDlg.hl.rehighlight() htmlDlg.show() htmlDlg.raise_() htmlDlg.activateWindow() def openHelp(self): QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki')) def aboutDialog(self): QMessageBox.about(self, self.aboutWindowTitle, '<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__)) +'</b></p>' + self.tr('Simple but powerful editor' ' for Markdown and reStructuredText') +'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011–2016') +'<br><a href="https://github.com/retext-project/retext">'+self.tr('Website') +'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">' +self.tr('Markdown syntax') +'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">' +self.tr('reStructuredText syntax')+'</a></p>') def setDefaultMarkup(self, markupClass): self.defaultMarkup = markupClass defaultName = markups.get_available_markups()[0].name writeToSettings('defaultMarkup', markupClass.name, defaultName) for tab in self.iterateTabs(): if not tab.fileName: tab.setMarkupClass(markupClass) tab.updatePreviewBox() self.docTypeChanged()
class AppMain(Ui_MainWindow): RAM_VIEW_INITIAL_SIZE = 10000 TEMP_MAX_RAM_USE = 1024 * 1000 STEP_TIMER_IN_MS = 1000 def __init__(self): Ui_MainWindow.__init__(self) ## class Variables self.config_dialog = None self.rom_stream = None self.rom_path = None self.rom_type_sel = None self.rom_model = None self.rom_watcher = None self.ram_model = None self.data_changed = None self.lst_parser = None self.last_step = None self.editor_converting = False self.pixmap_ALU = None self.asm_thread = None self.sim_thread = None self.config_dialog_ui = None self.actionROMGroup = None self.actionRAMGroup = None self.actionREGGroup = None self.step_timer = None self.window = AppMainWindow() # Setup Dialog, Editor, Actions, Threads, Img Resizing self.setup_dialog() self.setupUi(self.window) self.setup_editor() self.setup_actions() self.setup_threads() def show_alu(self): QDesktopServices.openUrl(QUrl.fromLocalFile("theme/alu.png")) def setup_editor(self): self.rom_stream = tempfile.SpooledTemporaryFile( max_size=self.TEMP_MAX_RAM_USE, mode="w+") self.rom_path = None self.rom_type_sel = self.actionROMAssembly self.lst_parser = None self.rom_watcher = QFileSystemWatcher() self.rom_watcher.fileChanged.connect(self.reload_rom) self.editor_converting = False self.spinBox.setValue(100) self.on_new() def setup_threads(self): self.asm_thread = QThread() self.sim_thread = QThread() def setup_dialog(self): self.config_dialog = QDialog() self.config_dialog_ui = config_dialog.Ui_Dialog() self.config_dialog_ui.setupUi(self.config_dialog) self.config_dialog_ui.assemblerLineEdit.setText( "../jar/Z01-Assembler.jar") self.config_dialog_ui.rtlLineEdit.setText("../Z01-Simulator-rtl-2/") def setup_clean_views(self, table, rows=100, caption="Dados", line_header=None): model = QStandardItemModel(rows, 1, self.window) model.setHorizontalHeaderItem(0, QStandardItem(caption)) table.setModel(model) for k in range(0, table.horizontalHeader().count()): table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) for l in range(0, rows): if line_header is None: model.setHeaderData(l, QtCore.Qt.Vertical, l) else: model.setHeaderData(l, QtCore.Qt.Vertical, line_header(l)) return model def setup_actions(self): self.step_timer = QtCore.QTimer() self.step_timer.timeout.connect(self.on_proximo) self.actionNovo.triggered.connect(self.on_new) self.actionSalvar_ROM.triggered.connect(self.on_save) self.actionAbrir.triggered.connect(self.on_load) self.actionProximo.triggered.connect(self.on_proximo) self.actionExecutarFim.triggered.connect(self.on_executar_fim) self.actionParar.triggered.connect(self.on_parar) self.actionEraseRAM.triggered.connect(self.on_clear_ram) self.actionVoltarInicio.triggered.connect(self.on_voltar_inicio) self.spinBox.valueChanged.connect(self.on_voltar_inicio) self.actionROMAssembly.triggered.connect(self.on_rom_assembly) self.actionROMBinario.triggered.connect(self.on_rom_binary) self.actionROMGroup = QActionGroup(self.window) self.actionROMGroup.addAction(self.actionROMAssembly) self.actionROMGroup.addAction(self.actionROMBinario) self.actionROMAssembly.setChecked(True) self.but_ALU.clicked.connect(self.show_alu) self.config_dialog_ui.procurarButton.clicked.connect( self.on_search_assembler) self.config_dialog_ui.alterarButton.clicked.connect( self.config_dialog.close) self.actionConfiguracoes.triggered.connect(self.config_dialog.show) def change_rtl_dir(self, new_dir): self.config_dialog_ui.rtlLineEdit.setText(new_dir) def on_rom_assembly(self): self.editor_converting = True file_utils.copy_file_to_model(self.rom_stream, self.rom_model) self.rom_type_sel = self.actionROMAssembly self.editor_converting = False def on_rom_binary(self): self.editor_converting = True if self.rom_type_sel == self.actionROMAssembly: file_utils.copy_model_to_file(self.rom_model, self.rom_stream) self.assemble(self.load_converted_asm_bin) self.rom_type_sel = self.actionROMBinario def on_ram_tooltip(self, item): text = item.text().strip() try: val = int(text, 2) except ValueError: return item.setToolTip("{0:d} dec - {1:x} hex".format(val, val)) def on_clear_ram(self): self.ram_model = self.setup_clean_views( self.ramView, rows=self.RAM_VIEW_INITIAL_SIZE, caption="RAM", line_header=asm_utils.z01_ram_name) for i in range(0, self.RAM_VIEW_INITIAL_SIZE): item = QStandardItem("0000000000000000") self.on_ram_tooltip(item) self.ram_model.setItem(i, item) def on_new(self): self.rom_path = None self.on_clear_ram() self.rom_model = self.setup_clean_views(self.romView, caption="ROM") self.rom_model.itemChanged.connect(self.valid_rom) self.ram_model.itemChanged.connect(self.valid_ram) self.actionROMAssembly.setEnabled(True) self.clear_simulation() def on_voltar_inicio(self): self.data_changed = True self.clear_simulation() def on_parar(self): self.step_timer.stop() def on_executar_fim(self): self.step_timer.start(self.STEP_TIMER_IN_MS) def show(self): self.window.show() def on_save(self): filename = self.rom_path if self.rom_path is not None: self.rom_watcher.removePath(self.rom_path) if filename is None: filename = QFileDialog.getSaveFileName(self.window, "Salve o arquivo", os.getcwd(), "Arquivos (*.hack *.nasm)") if len(filename) == 0 or len(filename[0]) == 0: return None filename = filename[0] self.rom_path = filename if self.actionROMAssembly.isChecked(): file_utils.copy_model_to_file(self.rom_model, self.rom_stream) file_utils.stream_to_file(self.rom_stream, filename) self.rom_watcher.addPath(self.rom_path) def on_load(self): filename = QFileDialog.getOpenFileName( self.window, "Escolha arquivo", os.getcwd(), "Arquivos (*.asm *.hack *.nasm)") if len(filename) == 0 or len(filename[0]) == 0: return None if self.rom_path is not None: self.rom_watcher.removePath(self.rom_path) self.on_new() self.rom_path = filename[0] self.rom_watcher.addPath(self.rom_path) self.reload_rom() def reload_rom(self): return self.load_rom(self.rom_path) def load_rom(self, filename): if not os.path.exists(filename): return if filename.endswith(".asm") or filename.endswith(".nasm"): self.load_asm(filename, self.rom_model) elif filename.endswith(".bin") or filename.endswith(".hack"): self.load_bin(filename, self.rom_model) def on_search_assembler(self): filename = QFileDialog.getOpenFileName(self.window, "Escolha arquivo", os.getcwd(), "Arquivo JAR (*.jar)") if len(filename) == 0 or len(filename[0]) == 0: return None self.config_dialog_ui.assemblerLineEdit.setText(filename[0]) def on_proximo(self): if self.data_changed: if self.lst_parser is not None: self.lst_parser.close() if self.actionROMAssembly.isChecked(): file_utils.copy_model_to_file(self.rom_model, self.rom_stream) self.assemble(self.assemble_end) else: tmp_rom = tempfile.SpooledTemporaryFile( max_size=self.TEMP_MAX_RAM_USE, mode="w+") file_utils.copy_model_to_file(self.rom_model, tmp_rom) tmp_ram = self.get_updated_ram() self.simulate(tmp_rom, tmp_ram) return False step = self.lst_parser.advance() if "s_regAout" not in step: self.step_timer.stop() QMessageBox.warning(self.window, "Simulador", "Fim de simulação") return self.update_line_edit(self.lineEdit_A, step["s_regAout"]) self.update_line_edit(self.lineEdit_S, step["s_regSout"]) self.update_line_edit(self.lineEdit_D, step["s_regDout"]) self.update_line_edit(self.lineEdit_inM, step["inM"]) self.update_line_edit(self.lineEdit_outM, step["outM"]) if self.last_step is not None: addr = int(step["s_regAout"], 2) index = self.ram_model.index(addr, 0) last_pc_counter = int(self.last_step["pcout"], 2) - 1 if int(step["writeM"]) == 0 and int( step["s_muxALUI_A"]) == 1 and int( self.last_step["s_muxALUI_A"]) == 0: self.ramView.setCurrentIndex(index) if int(step["writeM"]) == 1: self.ramView.setCurrentIndex(index) self.ram_model.itemFromIndex(index).setText(step["outM"]) else: last_pc_counter = -1 ## update ROM line pc_counter = int(step["pcout"], 2) - 1 if pc_counter < 0: pc_counter = 0 if pc_counter != last_pc_counter + 1: print("JUMP - Executando NOP") pc_counter = last_pc_counter # Mantem if self.actionROMAssembly.isChecked(): rom_line = asm_utils.z01_real_line(self.assembler_task.labels_pos, pc_counter) else: rom_line = pc_counter index = self.rom_model.index(rom_line, 0) self.romView.setCurrentIndex(index) print("PROXIMO") self.last_step = step def update_line_edit(self, line_edit, new_value, ignore=False): if line_edit.text() != new_value: line_edit.setText(new_value) if not ignore: line_edit.setStyleSheet( "QLineEdit {background-color: yellow;}") valid = self.valid_binary(line_edit) if valid: self.on_ram_tooltip(line_edit) else: line_edit.setStyleSheet("") def valid_rom(self, item): if not item.text(): return None text = item.text() index = item.index() while index.row() + 50 >= self.rom_model.rowCount(): self.rom_model.appendRow(QStandardItem("")) if self.actionROMAssembly.isChecked(): valid = asm_utils.z01_valid_assembly(item.text()) elif self.actionROMBinario.isChecked(): valid = self.valid_binary(item) else: valid = True if valid: if (self.actionROMBinario.isChecked() ) and self.editor_converting is False: self.actionROMAssembly.setEnabled(False) self.data_changed = True else: item.setText("") def valid_ram(self, item): if not item.text(): return None text = item.text() index = item.index() while index.row() + 100 >= self.ram_model.rowCount(): self.ram_model.appendRow(QStandardItem("{0:0>16b}".format(0))) if text.startswith("d"): text = text[1:] if text.isdigit(): item.setText("{0:0>16b}".format(int(text))) valid = self.valid_binary(item) if valid: #self.data_changed = True self.on_ram_tooltip(item) else: item.setText("{0:0>16b}".format(0)) def assemble(self, callback): if self.asm_thread.isRunning() or self.sim_thread.isRunning(): print("Assembler está sendo executado...") return False assembler = "java -jar " + self.config_dialog_ui.assemblerLineEdit.text( ) self.assembler_task = AssemblerTask(assembler, "temp/") rom_in = tempfile.SpooledTemporaryFile(max_size=self.TEMP_MAX_RAM_USE, mode="w+") rom_out = tempfile.SpooledTemporaryFile(max_size=self.TEMP_MAX_RAM_USE, mode="w+") file_utils.copy_file_to_file(self.rom_stream, rom_in) self.assembler_task.setup(rom_in, rom_out) self.assembler_task.finished.connect(callback) self.assembler_task.moveToThread(self.asm_thread) self.asm_thread.started.connect(self.assembler_task.run) self.asm_thread.start() def simulate(self, rom_file, ram_file): if self.asm_thread.isRunning() or self.sim_thread.isRunning(): print("Simulador está sendo executado...") return False self.simulator_task = SimulatorTask( "temp/", False, self.config_dialog_ui.simGUIBox.isChecked(), self.config_dialog_ui.rtlLineEdit.text()) lst_out = tempfile.SpooledTemporaryFile(max_size=self.TEMP_MAX_RAM_USE, mode="w+") self.simulator_task.setup(rom_file, ram_file, lst_out, self.spinBox.value() * 10 + 10) self.simulator_task.finished.connect(self.simulation_end) self.simulator_task.moveToThread(self.sim_thread) self.sim_thread.started.connect(self.simulator_task.run) self.sim_thread.start() self.lock_and_show_dialog() def lock_and_show_dialog(self): ## waits for ASM thread and SIM thread to end self.progress_dialog = QProgressDialog("Simulando...", "Cancelar", 0, 0, self.window) self.progress_dialog.setCancelButton(None) self.progress_dialog.setAutoReset(True) self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) self.progress_dialog.setMinimumDuration(0) self.progress_dialog.setValue(0) self.progress_dialog.setWindowTitle("RESimulatorGUI") self.progress_dialog.setWindowFlags(self.progress_dialog.windowFlags() & ~QtCore.Qt.WindowCloseButtonHint) while self.asm_thread.isRunning() or self.sim_thread.isRunning(): qapp.processEvents() self.progress_dialog.reset() def get_updated_ram(self): ram = tempfile.SpooledTemporaryFile(max_size=self.TEMP_MAX_RAM_USE, mode="w+") file_utils.copy_model_to_file(self.ram_model, ram) return ram def check_assembler_sucess(self): if self.assembler_task is not None and self.assembler_task.success is True: return True QMessageBox.critical(self.window, "Assembler", "Erro ao traduzir assembly.") self.step_timer.stop() return False def assemble_end(self): self.asm_thread.quit() # ensure end of thread self.asm_thread.wait() ram = self.get_updated_ram() if not self.check_assembler_sucess(): return print("ASM done!") self.simulate(self.assembler_task.stream_out, ram) def simulation_end(self): self.sim_thread.quit() #ensure end of thread self.sim_thread.wait() print("SIM done!") self.data_changed = False self.lst_parser = LSTParser(self.simulator_task.lst_stream) def load_converted_asm_bin(self): self.asm_thread.quit() self.asm_thread.wait() if not self.check_assembler_sucess(): return file_utils.copy_file_to_model(self.assembler_task.stream_out, self.rom_model) self.editor_converting = False def load_converted_asm_hex(self): self.asm_thread.quit() self.asm_thread.wait() if not self.check_assembler_sucess(): return file_utils.copy_file_to_model(self.assembler_task.stream_out, self.rom_model, asm_utils.bin_str_to_hex) self.editor_converting = False def valid_binary(self, item): valid = True text = item.text().strip() try: val = int(text, 2) except ValueError: valid = False if not valid: print("Invalid BIN Instruction: {}".format(item.text())) return valid def clear_simulation(self): self.last_step = None self.update_line_edit(self.lineEdit_A, "0000000000000000", True) self.update_line_edit(self.lineEdit_S, "0000000000000000", True) self.update_line_edit(self.lineEdit_D, "0000000000000000", True) self.update_line_edit(self.lineEdit_inM, "0000000000000000", True) self.update_line_edit(self.lineEdit_outM, "0000000000000000", True) self.data_changed = True index = self.ram_model.index(0, 0) self.ramView.setCurrentIndex(index) index = self.rom_model.index(0, 0) self.romView.setCurrentIndex(index) def load_file(self, filename, model): fp = open(filename, "r") counter = 0 lines = file_utils.file_len(filename) self.rom_model = self.setup_clean_views(self.romView, rows=lines + 200, caption="ROM") for i, l in enumerate(fp): if asm_utils.z01_valid_assembly(l.strip()): index = self.rom_model.index(counter, 0) self.rom_model.itemFromIndex(index).setText(l.strip()) counter += 1 fp.close() def load_asm(self, filename, model): self.actionROMAssembly.setChecked(True) self.load_file(filename, model) def load_bin(self, filename, model): self.actionROMBinario.setChecked(True) self.actionROMAssembly.setEnabled(False) self.load_file(filename, model)