class PlainView(QFrame): textChanged = pyqtSignal() def __init__(self, text): super(PlainView, self).__init__() layout = QVBoxLayout() self._plain_text = QPlainTextEdit(text) self._plain_text.textChanged.connect(self._on_plain_text_changed_event) self._search_field = SearchField() self._search_field.setClosable(True) self._search_field.setIcon(qtawesome.icon("fa.search")) self._search_field.setPlaceholderText("Search text") self._search_field.escapePressed.connect( self._on_search_field_escape_pressed_event) self._search_field.textChanged.connect(self._do_highlight_text) self._search_field.closeEvent.connect(self._do_close_search_field) self._search_field.setVisible(False) layout.addWidget(self._plain_text) layout.addWidget(self._search_field) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) def _on_plain_text_changed_event(self): self.textChanged.emit() if self._search_field.isVisible(): self._do_highlight_text() def _on_search_field_escape_pressed_event(self): if self._search_field.hasFocus() and self._search_field.isVisible(): self._do_close_search_field() def _do_highlight_clear(self): self._plain_text.blockSignals(True) format = QTextCharFormat() format.setForeground(QBrush(QColor("black"))) cursor = self._plain_text.textCursor() cursor.setPosition(0) cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor, 1) cursor.mergeCharFormat(format) self._plain_text.blockSignals(False) def _do_highlight_text(self): def highlight_text(text, format): cursor = self._plain_text.textCursor() regex = QRegExp(QRegExp.escape(text)) # Process the displayed document pos = 0 index = regex.indexIn(self._plain_text.toPlainText(), pos) while index != -1: # Select the matched text and apply the desired format cursor.setPosition(index) cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, len(text)) cursor.mergeCharFormat(format) # Move to the next match pos = index + regex.matchedLength() index = regex.indexIn(self.toPlainText(), pos) self._do_highlight_clear() self._plain_text.blockSignals(True) searchString = self._search_field.text() if searchString: format = QTextCharFormat() format.setForeground(QBrush(QColor("red"))) highlight_text(searchString, format) self._plain_text.blockSignals(False) def _do_open_search_field(self): self._search_field.setVisible(True) self._do_highlight_text() self._search_field.setFocus() def _do_close_search_field(self): self._do_highlight_clear() self._plain_text.setFocus() self._search_field.setVisible(False) def toggleSearchField(self): if self._search_field.hasFocus() and self._search_field.isVisible(): self._do_close_search_field() else: self._do_open_search_field() def toPlainText(self): return self._plain_text.toPlainText() def setPlainText(self, text): return self._plain_text.setPlainText(text) def setFocus(self, Qt_FocusReason=None): self._plain_text.setFocus()
class ApplicationPage(QWidget): """ The GUI for the application page of a project. """ # The page's label. label = "Application Source" @property def project(self): """ The project property getter. """ return self._project @project.setter def project(self, value): """ The project property setter. """ if self._project != value: self._project = value self._script_edit.set_project(value) self._package_edit.set_project(value) self._update_page() def __init__(self): """ Initialise the page. """ super().__init__() self._project = None # Create the page's GUI. layout = QGridLayout() form = BetterForm() self._name_edit = QLineEdit( placeholderText="Application name", whatsThis="The name of the application. It will default to " "the base name of the application script without any " "extension.", textEdited=self._name_changed) form.addRow("Name", self._name_edit) self._script_edit = FilenameEditor( "Application Script", placeholderText="Application script", whatsThis="The name of the application's optional main script " "file.", textEdited=self._script_changed) form.addRow("Main script file", self._script_edit) self._entry_point_edit = QLineEdit( placeholderText="Entry point in application package", whatsThis="The name of the optional entry point in the " "application's package.", textEdited=self._entry_point_changed) form.addRow("Entry point", self._entry_point_edit) self._sys_path_edit = QLineEdit( placeholderText="Additional sys.path directories", whatsThis="A space separated list of additional directories, " "ZIP files and eggs to add to <tt>sys.path</tt>. Only " "set this if you want to allow external packages to " "be imported.", textEdited=self._sys_path_changed) form.addRow("sys.path", self._sys_path_edit) layout.addLayout(form, 0, 0) options_layout = BetterForm() self._console_edit = QCheckBox( "Use console (Windows)", whatsThis="Enable console output for Windows applications. " "Console output will be enabled automatically if no " "graphical PyQt modules are used.", stateChanged=self._console_changed) options_layout.addRow(self._console_edit) self._bundle_edit = QCheckBox( "Application bundle (macOS)", whatsThis="Build an application bundle on macOS. If it is not " "checked then the application will be built as a " "simple executable.", stateChanged=self._bundle_changed) options_layout.addRow(self._bundle_edit) layout.addLayout(options_layout, 0, 1) # Extra space is needed before the application package editor. layout.setRowMinimumHeight( 1, 1.4 * QFontInfo(QGuiApplication.font()).pixelSize()) self._package_edit = _ApplicationPackageEditor() self._package_edit.package_changed.connect(self._package_changed) package_edit_gb = QGroupBox(self._package_edit.title) package_edit_gb.setFlat(True) package_edit_gb.setLayout(self._package_edit) layout.addWidget(package_edit_gb, 2, 0, 1, 2) qmake = CollapsibleWidget("Additional qmake Configuration") self._qmake_edit = QPlainTextEdit( whatsThis="Any text entered here will be appended to the " "generated <tt>.pro</tt> that will be processed by " "<tt>qmake</tt>.", textChanged=self._qmake_changed) qmake.setWidget(self._qmake_edit) layout.addWidget(qmake, 3, 0, 1, 2) self.setLayout(layout) def _update_page(self): """ Update the page using the current project. """ project = self.project self._name_edit.setText(project.application_name) self._script_edit.setText(project.application_script) self._entry_point_edit.setText(project.application_entry_point) self._sys_path_edit.setText(project.sys_path) self._package_edit.configure(project.application_package, project) blocked = self._console_edit.blockSignals(True) self._console_edit.setCheckState( Qt.Checked if project.application_is_console else Qt.Unchecked) self._console_edit.blockSignals(blocked) blocked = self._bundle_edit.blockSignals(True) self._bundle_edit.setCheckState( Qt.Checked if project.application_is_bundle else Qt.Unchecked) self._bundle_edit.blockSignals(blocked) blocked = self._qmake_edit.blockSignals(True) self._qmake_edit.setPlainText(self._project.qmake_configuration) self._qmake_edit.blockSignals(blocked) def _console_changed(self, state): """ Invoked when the user changes the console state. """ self.project.application_is_console = (state == Qt.Checked) self.project.modified = True def _bundle_changed(self, state): """ Invoked when the user changes the bundle state. """ self.project.application_is_bundle = (state == Qt.Checked) self.project.modified = True def _name_changed(self, value): """ Invoked when the user edits the application name. """ self.project.application_name = value self.project.modified = True def _script_changed(self, value): """ Invoked when the user edits the application script name. """ self.project.application_script = value self.project.modified = True def _entry_point_changed(self, value): """ Invoked when the user edits the entry point. """ self.project.application_entry_point = value self.project.modified = True def _sys_path_changed(self, value): """ Invoked when the user edits the sys.path directories. """ self.project.sys_path = value.strip() self.project.modified = True def _package_changed(self): """ Invoked when the user edits the application package. """ self.project.modified = True def _qmake_changed(self): """ Invoked when the user edits the qmake configuration. """ self.project.qmake_configuration = self._qmake_edit.toPlainText() self.project.modified = True
class LogViewer(QWidget): def __init__(self, show_indicators=False, indicator='', file_name='', parent=None): super(LogViewer, self).__init__(parent) # Setup layout global_layout = QHBoxLayout(self) global_layout.setContentsMargins(0, 0, 0, 0) widget = QWidget(self) widget.setObjectName('logview') global_layout.addWidget(widget) layout = QVBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) self.project_path = '' self.log_path = '' self.error_path = '' self.indicator_names = ('log', 'error', 'sys_log', 'notes') self.indicator = indicator self.text = QPlainTextEdit(widget) if file_name: self.text.setObjectName('dialog') else: self.text.setObjectName('status') self.text.setPlaceholderText('Welcome to TranSPHIRE!') self.text.setToolTip( 'Double click after starting TranSPHIRE in order to show more information' ) self.text.setReadOnly(True) self.text.setWordWrapMode(QTextOption.WrapAnywhere) layout.addWidget(self.text, stretch=1) self.file_name = file_name self.buttons = {} if show_indicators: layout_h1 = QHBoxLayout() for entry in self.indicator_names: template = '{0}: {{0}}'.format(entry) self.buttons[entry] = [QPushButton(self), template] self.buttons[entry][0].setObjectName('button_entry') self.buttons[entry][0].clicked.connect(self.my_click_event) layout_h1.addWidget(self.buttons[entry][0]) self.increment_indicator(entry, '0') layout_h1.addStretch(1) layout.addLayout(layout_h1) self.change_state(False) self.update_plain_text(force=True) self.timer = QTimer(self) self.timer.setInterval(1000) self.timer.timeout.connect(self.update_plain_text) self.timer.start() if self.indicator == 'notes': layout_h = QHBoxLayout() layout_h.setContentsMargins(0, 0, 0, 0) self.input_edit = QLineEdit('', self) submit_button = QPushButton('Submit', self) submit_button.clicked.connect(self.submit_text) layout_h.addWidget(self.input_edit, stretch=1) layout_h.addWidget(submit_button) layout.addLayout(layout_h) @pyqtSlot() def update_plain_text(self, force=False): if self.file_name and os.path.exists(self.file_name): with open(self.file_name, 'r') as read: text = read.read() if force: self.reset_plain_text(text) elif text.replace('\n', '').replace( ' ', '') != self.text.toPlainText().replace( '\n', '').replace(' ', ''): self.reset_plain_text(text) def reset_plain_text(self, text): self.text.setPlainText(text) cursor = self.text.textCursor() cursor.movePosition(QTextCursor.End) self.text.setTextCursor(cursor) @pyqtSlot() def submit_text(self): self.appendPlainText(self.input_edit.text(), indicator='notes', user=True) self.input_edit.setText('') def appendPlainText(self, text, indicator='log', user=False): text_raw = tu.create_log(text) prefix, suffix = text_raw.split(' => ', 1) if user: text = '{}\n{}: {}\n'.format(prefix, getpass.getuser(), suffix) else: text = '{}\n{}\n'.format(prefix, suffix) try: with open(self.file_name, 'a+') as write: write.write(text) except IOError: pass self.text.appendPlainText(text) cursor = self.text.textCursor() cursor.movePosition(QTextCursor.End) self.text.setTextCursor(cursor) print(text) if self.project_path: self.increment_indicator(indicator) def increment_indicator(self, indicator, text=''): if indicator in self.indicator_names: button, template = self.buttons[indicator] if text: cur_text = text else: cur_text = str(1 + int(self.get_indicator(indicator))) button.setText(template.format(cur_text)) button.setToolTip(template.format(text)) if self.get_indicator(indicator) == '0': button.setStyleSheet('') else: button.setStyleSheet(tu.get_style('changed')) else: assert False, indicator def get_indicator(self, indicator): if indicator in self.indicator_names: return self.buttons[indicator][0].text().split(':')[-1].strip() else: assert False, indicator @pyqtSlot() def my_click_event(self, event=None): if not self.project_path: return None sender = self.sender() sender_text = sender.text().split(':')[0].strip() is_notes = False file_path = self.log_path if sender_text == 'log': file_names = ['log.txt'] elif sender_text == 'notes': is_notes = True file_names = ['notes.txt'] elif sender_text == 'sys_log': file_names = ['sys_log.txt'] elif sender_text == 'error': file_names = [ os.path.basename(entry) for entry in glob.glob(os.path.join(self.error_path, '*')) ] file_path = self.error_path else: assert False, sender.text() if not is_notes: self.increment_indicator(sender_text, '0') sender.setEnabled(False) QTimer.singleShot(5000, lambda: sender.setEnabled(True)) dialog = logviewerdialog.LogViewerDialog(self) for file_name in file_names: dialog.add_tab( LogViewer(file_name=os.path.join(file_path, file_name), indicator=sender_text, parent=self), os.path.basename(file_name), ) dialog.show() @pyqtSlot(str, str, str) def set_project_path(self, project_path, log_path, error_path): self.project_path = project_path self.log_path = log_path self.error_path = error_path state = True if not self.project_path: state = False self.file_name = '' elif not self.file_name: self.file_name = os.path.join(self.log_path, 'log.txt') self.update_plain_text(force=True) self.change_state(state) def change_state(self, state): self.text.blockSignals(not state) for button, _ in self.buttons.values(): button.setEnabled(state) button.blockSignals(not state)