def init_splitter(self): self.frame = QFrame() self.frame.setFrameShape(QFrame.StyledPanel) hbox = QHBoxLayout() # here should be no 'self' argument self.browser_widget = bv.BrowserWidget(self) self.top_splitter = QSplitter(Qt.Horizontal) frame = QFrame() layout = QVBoxLayout() for addon in self.addon_list: layout.addWidget(addon) addon.hide() frame.setLayout(layout) self.top_splitter.addWidget(frame) self.current_addon = self.addon_list[0] self.current_addon.show_itself() self.top_splitter.addWidget(self.browser_widget) self.top_splitter.setSizes([100, 200]) handle_width = 6 # https://stackoverflow.com/questions/2545577/qsplitter-becoming-undistinguishable-between-qwidget-and-qtabwidget self.top_splitter.setOpaqueResize(False) self.top_splitter.setChildrenCollapsible(False) self.top_splitter.setHandleWidth(handle_width) splitter2 = QSplitter(Qt.Vertical) splitter2.addWidget(self.top_splitter) splitter2.addWidget(self.jupyter_widget) splitter2.setOpaqueResize(False) splitter2.setChildrenCollapsible(False) splitter2.setHandleWidth(handle_width) hbox.addWidget(splitter2) self.frame.setLayout(hbox)
def initUI(self): """Override.""" ctrl_layout = QHBoxLayout() AT = Qt.AlignTop ctrl_layout.addWidget(self._roi_ctrl_widget) ctrl_layout.addWidget(self._roi_fom_ctrl_widget, alignment=AT) ctrl_layout.addWidget(self._roi_hist_ctrl_widget, alignment=AT) ctrl_layout.addWidget(self._roi_norm_ctrl_widget, alignment=AT) ctrl_layout.addWidget(self._roi_proj_ctrl_widget, alignment=AT) subview_splitter = QSplitter(Qt.Vertical) subview_splitter.setHandleWidth(9) subview_splitter.setChildrenCollapsible(False) subview_splitter.addWidget(self._roi_proj_plot) subview_splitter.addWidget(self._roi_hist) view_splitter = QSplitter(Qt.Horizontal) view_splitter.setHandleWidth(9) view_splitter.setChildrenCollapsible(False) view_splitter.addWidget(self._corrected) view_splitter.addWidget(subview_splitter) layout = QVBoxLayout() layout.addWidget(view_splitter) layout.addLayout(ctrl_layout) self.setLayout(layout)
def sig_to_stems_clicked(self, row): signature = self.sig_to_stems_major_table.item(row, 0).text() print(signature) signature = tuple(signature.split(SEP_SIG)) stems = sorted(self.lexicon.signatures_to_stems()[signature]) number_of_stems_per_column = 5 # create a master list of sublists, where each sublist contains k stems # k = number_of_stems_per_column stem_rows = list() stem_row = list() for i, stem in enumerate(stems, 1): stem_row.append(stem) if not i % number_of_stems_per_column: stem_rows.append(stem_row) stem_row = list() if stem_row: stem_rows.append(stem_row) # set up the minor table as table widget sig_to_stems_minor_table = QTableWidget() sig_to_stems_minor_table.horizontalHeader().hide() sig_to_stems_minor_table.verticalHeader().hide() sig_to_stems_minor_table.clear() sig_to_stems_minor_table.setRowCount(len(stem_rows)) sig_to_stems_minor_table.setColumnCount(number_of_stems_per_column) # fill in the minor table for row, stem_row in enumerate(stem_rows): for col, stem in enumerate(stem_row): item = QTableWidgetItem(stem) sig_to_stems_minor_table.setItem(row, col, item) sig_to_stems_minor_table.resizeColumnsToContents() minor_table_title = QLabel('{} (number of stems: {})' .format(SEP_SIG.join(signature), len(stems)) ) minor_table_widget_with_title = QWidget() layout = QVBoxLayout() layout.addWidget(minor_table_title) layout.addWidget(sig_to_stems_minor_table) minor_table_widget_with_title.setLayout(layout) new_display = QSplitter(Qt.Horizontal) new_display.setHandleWidth(10) new_display.setChildrenCollapsible(False) new_display.addWidget(self.sig_to_stems_major_table) new_display.addWidget(minor_table_widget_with_title) new_display_width = self.majorDisplay.width() / 2 new_display.setSizes( [new_display_width * 0.4, new_display_width * 0.6]) self.load_main_window(major_display=new_display) self.status.clearMessage() self.status.showMessage('{} selected'.format(signature))
def initUI(self): """Override.""" ctrl_widget = QWidget() ctrl_layout = QHBoxLayout() AT = Qt.AlignTop ctrl_layout.addWidget(self._roi_ctrl_widget) ctrl_layout.addWidget(self._roi_fom_ctrl_widget, alignment=AT) ctrl_layout.addWidget(self._roi_hist_ctrl_widget, alignment=AT) ctrl_layout.addWidget(self._roi_norm_ctrl_widget, alignment=AT) ctrl_layout.addWidget(self._roi_proj_ctrl_widget, alignment=AT) ctrl_layout.setContentsMargins(1, 1, 1, 1) ctrl_widget.setLayout(ctrl_layout) ctrl_widget.setFixedHeight( self._roi_proj_ctrl_widget.minimumSizeHint().height()) subview_splitter = QSplitter(Qt.Vertical) subview_splitter.setHandleWidth(9) subview_splitter.setChildrenCollapsible(False) subview_splitter.addWidget(self._roi_proj_plot) subview_splitter.addWidget(self._roi_hist) view_splitter = QSplitter(Qt.Horizontal) view_splitter.setHandleWidth(9) view_splitter.setChildrenCollapsible(False) view_splitter.addWidget(self._corrected) view_splitter.addWidget(subview_splitter) view_splitter.setSizes([1e6, 1e6]) layout = QVBoxLayout() layout.addWidget(view_splitter) layout.addWidget(ctrl_widget) layout.setContentsMargins(1, 1, 1, 1) self.setLayout(layout)
def initUI(self): """Override.""" self._monitor_tb.setTabPosition(QTabWidget.TabPosition.South) self._monitor_tb.addTab(self._avail_src_view, "Available sources") self._monitor_tb.addTab(self._process_mon_view, "Process monitor") splitter = QSplitter(Qt.Vertical) splitter.setHandleWidth(self.SPLITTER_HANDLE_WIDTH) splitter.setChildrenCollapsible(False) splitter.addWidget(self._con_view) splitter.addWidget(self._src_view) splitter.addWidget(self._monitor_tb) splitter.setStretchFactor(0, 3) splitter.setStretchFactor(1, 1) h = splitter.sizeHint().height() splitter.setSizes([0.1 * h, 0.6 * h, 0.3 * h]) layout = QVBoxLayout() layout.addWidget(splitter) self.setLayout(layout) self._con_view.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) self._src_view.expandToDepth(1) self._src_view.resizeColumnToContents(0) self._src_view.resizeColumnToContents(1)
class MrhReadWidget(QWidget): """Tab READ.""" def __init__(self): super().__init__() self.datatable = MrhReadTable() self.datatable.setObjectName('read_datatable') self.viewoptiongroup = MrhReadViewGroupbox() self.viewoptiongroup.setObjectName('read_viewoption') self.info_textedit = QTextEdit() self.info_textedit.setObjectName('read_info_textedit') self.functiongroup = MrhReadFunctionGroupbox() self.functiongroup.setObjectName('read_functiongroup') self.memooptiongroup = MrhReferenceMemoGroupbox() self.memooptiongroup.setObjectName('read_memooption') self.vlayout = QVBoxLayout() self.splitter = QSplitter() self.splitter.setChildrenCollapsible(False) self.splitter.setOrientation(Qt.Vertical) self.splitter.addWidget(self.datatable) self.splitter.addWidget(self.info_textedit) self.splitter.addWidget(self.memooptiongroup) self.vlayout.addWidget(self.splitter) self.vlayout.addWidget(self.viewoptiongroup) self.vlayout.addWidget(self.functiongroup) self.setLayout(self.vlayout)
def sig_to_stems_clicked(self, row): signature = self.sig_to_stems_major_table.item(row, 0).text() print(signature) signature = tuple(signature.split(SEP_SIG)) stems = sorted(self.lexicon.signatures_to_stems()[signature]) number_of_stems_per_column = 5 # create a master list of sublists, where each sublist contains k stems # k = number_of_stems_per_column stem_rows = list() stem_row = list() for i, stem in enumerate(stems, 1): stem_row.append(stem) if not i % number_of_stems_per_column: stem_rows.append(stem_row) stem_row = list() if stem_row: stem_rows.append(stem_row) # set up the minor table as table widget sig_to_stems_minor_table = QTableWidget() sig_to_stems_minor_table.horizontalHeader().hide() sig_to_stems_minor_table.verticalHeader().hide() sig_to_stems_minor_table.clear() sig_to_stems_minor_table.setRowCount(len(stem_rows)) sig_to_stems_minor_table.setColumnCount(number_of_stems_per_column) # fill in the minor table for row, stem_row in enumerate(stem_rows): for col, stem in enumerate(stem_row): item = QTableWidgetItem(stem) sig_to_stems_minor_table.setItem(row, col, item) sig_to_stems_minor_table.resizeColumnsToContents() minor_table_title = QLabel('{} (number of stems: {})' .format(SEP_SIG.join(signature), len(stems))) minor_table_widget_with_title = QWidget() layout = QVBoxLayout() layout.addWidget(minor_table_title) layout.addWidget(sig_to_stems_minor_table) minor_table_widget_with_title.setLayout(layout) new_display = QSplitter(Qt.Horizontal) new_display.setHandleWidth(10) new_display.setChildrenCollapsible(False) new_display.addWidget(self.sig_to_stems_major_table) new_display.addWidget(minor_table_widget_with_title) new_display_width = self.majorDisplay.width() / 2 new_display.setSizes( [new_display_width * 0.4, new_display_width * 0.6]) self.load_main_window(major_display=new_display) self.status.clearMessage() self.status.showMessage('{} selected'.format(signature))
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 getSplitter(self): 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.editBox, self.previewBox: widget.setMinimumWidth(125) splitter.addWidget(widget) splitter.setSizes((50, 50)) splitter.setChildrenCollapsible(False) return splitter
class Sidebar(QWidget): def __init__(self): super().__init__() self.setWindowTitle = "Longway Sidebar" self.setGeometry(1920 - 180, 0, 180, 1080 - 50) self.layout = QVBoxLayout() self.layout.setSpacing(0) self.layout.setContentsMargins(0, 0, 0, 0) self.setStyleSheet( 'background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop: 0 #4d77b3, stop: 0.015 #67a6d0, stop: 0.03 #5578ac, stop: 1 #3364af)') self.gadgets = [] self.splitter = QSplitter(Qt.Vertical) self.splitter.setChildrenCollapsible(False) self.splitter.setMinimumHeight(20) self.clickable = QPushButton() self.clickable.setStyleSheet(''' QPushButton { background-color: #00ffffff; } QPushButton:pressed { background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop: 0 #111166, stop: 1 #222277); } QPushButton:hover { background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop: 0 #4685d9, stop: 1 #89c8f1); }; height: 24px; border: none; color:white; font-weight: bold;''') self.clickable.setText('Add Gadget') self.clickable.clicked.connect(self.add_gadget) for i in range(0): r = randint(0, 1) if r == 0: self.gadgets.append(GadgetText()) elif r == 1: self.gadgets.append(GadgetWeb()) # self.gadgets[-1].setFrameShape(QFrame.StyledPanel) self.splitter.addWidget(self.gadgets[-1]) self.layout.addWidget(self.splitter) self.layout.addWidget(self.clickable) self.setLayout(self.layout) self.setWindowFlags(Qt.FramelessWindowHint) self.show() def add_gadget(self): self.gadgets.append(GadgetText()) self.splitter.addWidget(self.gadgets[-1])
def init_ui(self): main_box = QHBoxLayout() hsplitter = QSplitter(Qt.Horizontal) self.init_peer_list() hsplitter.addWidget(self.peer_list) message_box = self.create_message_box() hsplitter.addWidget(message_box) hsplitter.setChildrenCollapsible(False) hsplitter.setSizes([1000, 3000]) main_box.addWidget(hsplitter, 1) main_box.setContentsMargins(0, 0, 0, 0) self.setLayout(main_box)
def initUI(self): """Override.""" view_tab = QTabWidget() view_tab.setTabPosition(QTabWidget.TabPosition.South) view_tab.addTab(self._corrected, "Corrected") view_tab.addTab(self._q_view, "Momentum transfer (q)") view_splitter = QSplitter() view_splitter.setChildrenCollapsible(False) view_splitter.addWidget(view_tab) view_splitter.addWidget(self._azimuthal_integ_1d_curve) layout = QVBoxLayout() layout.addWidget(view_splitter) layout.addWidget(self._ctrl_widget) self.setLayout(layout)
class ProfitView(AccountSheetView): def _setup(self): self._setupUi() self.sheet = self.psheet = ProfitSheet(self.model.istatement, view=self.treeView) self.graph = self.pgraph = Chart(self.model.pgraph, view=self.graphView) self.piechart = Chart(self.model.pie, view=self.pieChart) def _setupUi(self): self.resize(558, 447) self.mainLayout = QVBoxLayout(self) self.mainLayout.setSpacing(0) self.mainLayout.setContentsMargins(0, 0, 0, 0) self.splitterView = QSplitter() self.splitterView.setChildrenCollapsible(False) self.splitterView.setOrientation(Qt.Vertical) self.subSplitterView = QSplitter() self.subSplitterView.setChildrenCollapsible(False) self.treeView = TreeView(self) self.treeView.setAcceptDrops(True) self.treeView.setFrameShape(QFrame.NoFrame) self.treeView.setFrameShadow(QFrame.Plain) self.treeView.setEditTriggers(QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked) self.treeView.setDragEnabled(True) self.treeView.setDragDropMode(QAbstractItemView.InternalMove) self.treeView.setUniformRowHeights(True) self.treeView.setAllColumnsShowFocus(True) self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView.header().setStretchLastSection(False) self.subSplitterView.addWidget(self.treeView) self.pieChart = PieChartView(self) self.pieChart.setMinimumSize(300, 0) self.subSplitterView.addWidget(self.pieChart) self.splitterView.addWidget(self.subSplitterView) self.graphView = BarGraphView(self) self.graphView.setMinimumSize(0, 200) self.splitterView.addWidget(self.graphView) self.splitterView.setStretchFactor(0, 1) self.splitterView.setStretchFactor(1, 0) self.subSplitterView.setStretchFactor(0, 1) self.subSplitterView.setStretchFactor(1, 0) self.mainLayout.addWidget(self.splitterView)
class NetWorthView(AccountSheetView): def _setup(self): self._setupUi() self.sheet = self.nwsheet = NetWorthSheet(self.model.bsheet, view=self.treeView) self.graph = self.nwgraph = Chart(self.model.nwgraph, view=self.graphView) self.piechart = Chart(self.model.pie, view=self.pieChart) def _setupUi(self): self.resize(558, 447) self.mainLayout = QVBoxLayout(self) self.mainLayout.setSpacing(0) self.mainLayout.setContentsMargins(0, 0, 0, 0) self.splitterView = QSplitter() self.splitterView.setChildrenCollapsible(False) self.splitterView.setOrientation(Qt.Vertical) self.subSplitterView = QSplitter() self.subSplitterView.setChildrenCollapsible(False) self.treeView = TreeView(self) self.treeView.setAcceptDrops(True) self.treeView.setFrameShape(QFrame.NoFrame) self.treeView.setFrameShadow(QFrame.Plain) self.treeView.setEditTriggers(QAbstractItemView.EditKeyPressed|QAbstractItemView.SelectedClicked) self.treeView.setDragEnabled(True) self.treeView.setDragDropMode(QAbstractItemView.InternalMove) self.treeView.setUniformRowHeights(True) self.treeView.setAllColumnsShowFocus(True) self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView.header().setStretchLastSection(False) self.subSplitterView.addWidget(self.treeView) self.pieChart = PieChartView(self) self.pieChart.setMinimumSize(300, 0) self.subSplitterView.addWidget(self.pieChart) self.splitterView.addWidget(self.subSplitterView) self.graphView = LineGraphView(self) self.graphView.setMinimumSize(0, 200) self.splitterView.addWidget(self.graphView) self.splitterView.setStretchFactor(0, 1) self.splitterView.setStretchFactor(1, 0) self.subSplitterView.setStretchFactor(0, 1) self.subSplitterView.setStretchFactor(1, 0) self.mainLayout.addWidget(self.splitterView)
def decorate(splitter: QSplitter, index: int = 1): gripLength = 35 gripWidth = 1 # may need to be 1 or 2 depending on theme gripSpacing = 0 grips = 3 splitter.setOpaqueResize(False) splitter.setChildrenCollapsible(False) splitter.setHandleWidth(7) handle = splitter.handle(index) orientation = splitter.orientation() layout = QHBoxLayout(handle) layout.setSpacing(gripSpacing) layout.setContentsMargins(0, 0, 0, 0) if orientation == Qt.Horizontal: for i in range(grips): line = QFrame(handle) line.setMinimumSize(gripWidth, gripLength) line.setMaximumSize(gripWidth, gripLength) line.setLineWidth(gripWidth) line.setFrameShape(line.StyledPanel) line.setStyleSheet("border: 1px solid lightgray;") layout.addWidget(line) else: # center the vertical grip by adding spacers before and after layout.addStretch() vBox = QVBoxLayout() for i in range(grips): line = QFrame(handle) line.setMinimumSize(gripLength, gripWidth) line.setMaximumSize(gripLength, gripWidth) line.setFrameShape(line.StyledPanel) line.setStyleSheet("border: 1px solid lightgray;") vBox.addWidget(line) layout.addLayout(vBox) layout.addStretch()
class PlaylistTab(QWidget): '''A widget for administrating the all models of the playlist data.''' def __init__(self, parent=None): super().__init__(parent) self.setObjectName('tabPlaylist') self.horizontalLayout = QHBoxLayout(self) self.horizontalLayout.setObjectName("horizontalLayoutPlaylist") self.horizontalLayout.setContentsMargins(6, 6, 6, 6) self.horizontalLayout.setSpacing(6) self.horizontalSplitter = QSplitter(self) self.horizontalSplitter.setObjectName("horizontalSplitterPlaylist") self.horizontalSplitter.setChildrenCollapsible(False) # Artists/Albums/Games Layout Widget self.verticalSplitter = QSplitter(Qt.Vertical, self) self.verticalSplitter.setObjectName('verticalSplitterPlaylist') self.verticalSplitter.setChildrenCollapsible(False) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(1) sizePolicy.setHeightForWidth( self.verticalSplitter.sizePolicy().hasHeightForWidth()) self.verticalSplitter.setSizePolicy(sizePolicy) self.horizontalSplitter.addWidget(self.verticalSplitter) # Create the group box widgets for the playlist items self.groupBoxArtists = ArtistGroupBox(self) self.groupBoxAlbums = AlbumGroupBox(self) self.groupBoxGames = GameGroupBox(self) self.groupBoxSongs = SongGroupBox(self) # Add group boxes to layouts self.verticalSplitter.addWidget(self.groupBoxArtists) self.verticalSplitter.addWidget(self.groupBoxAlbums) self.verticalSplitter.addWidget(self.groupBoxGames) self.horizontalSplitter.addWidget(self.groupBoxSongs) self.horizontalLayout.addWidget(self.horizontalSplitter)
def __init__(self, channelName, clientIRC, jsonDecoder): super(ChatTab, self).__init__() userList = UserList(self) self.userList = userList self.setAttribute(Qt.WA_DeleteOnClose) self.clientIRC = clientIRC channelChat = ChannelChat(self, channelName, jsonDecoder) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) splitter = QSplitter(Qt.Horizontal) splitter.addWidget(channelChat) splitter.addWidget(userList) splitter.setContentsMargins(0, 0, 0, 0) splitter.setHandleWidth(0) splitter.setStretchFactor(0, 7) splitter.setStretchFactor(1, 1) splitter.setChildrenCollapsible(False) layout.addWidget(splitter) self.jsonDecoder = jsonDecoder self.clientIRC.joinChannel(channelName) self.setLayout(layout) self.channelChat = channelChat self.channelName = '#' + channelName
class MrhClassifyWidget(QWidget): """Tab Classify.""" def __init__(self): super().__init__() self.treelist = MrhClassifyTree() self.treelist.setObjectName('classify_treelist') self.info_textedit = QTextEdit() self.info_textedit.setObjectName('classify_info_textedit') self.memooptiongroup = MrhReferenceMemoGroupbox() self.memooptiongroup.setObjectName('classify_memooption') self.functiongroup = MrhClassifyFunctionGroupbox() self.functiongroup.setObjectName('classify_functiongroup') self.vlayout = QVBoxLayout() self.splitter = QSplitter() self.splitter.setChildrenCollapsible(False) self.splitter.setOrientation(Qt.Vertical) self.splitter.addWidget(self.treelist) self.splitter.addWidget(self.info_textedit) self.splitter.addWidget(self.memooptiongroup) self.vlayout.addWidget(self.splitter) self.vlayout.addWidget(self.functiongroup) self.setLayout(self.vlayout)
class MainWindow(QMainWindow): def __init__(self, screen_height, screen_width, version, parent=None): super(MainWindow, self).__init__(parent) self.screen_width = screen_width self.screen_height = screen_height self.version = version # basic main window settings self.resize(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT) self.setWindowTitle('Linguistica {}'.format(self.version)) # lexicon and lexicon tree self.lexicon = None self.lexicon_tree = None self.initialize_lexicon_tree() # set up major display, parameter window, then load main window self.majorDisplay = QWidget() self.parameterWindow = QWidget() self.load_main_window() # 'File' menu and actions select_corpus_action = self.create_action(text='&Select corpus...', slot=self.corpus_dir_dialog, tip='Select a corpus file', shortcut='Ctrl+N') select_wordlist_action = self.create_action(text='&Select wordlist...', slot=self.wordlist_dir_dialog, tip='Select a wordlist file', shortcut='Ctrl+W') run_file_action = self.create_action(text='&Run...', slot=self.run_file, tip='Run the input file', shortcut='Ctrl+D') parameters_action = self.create_action(text='&Parameters...', slot=self.parameters_dialog, tip='Change parameters', shortcut='Ctrl+P') file_menu = self.menuBar().addMenu('&File') file_menu.addAction(select_corpus_action) file_menu.addAction(select_wordlist_action) file_menu.addAction(run_file_action) file_menu.addAction(parameters_action) self.status = self.statusBar() self.status.setSizeGripEnabled(False) self.status.showMessage('No input file loaded. To select one: File --> ' 'Select corpus... or Select wordlist...') def initialize_lexicon_tree(self): self.lexicon_tree = QTreeWidget() self.lexicon_tree.setEnabled(True) self.lexicon_tree.setMinimumWidth(TREEWIDGET_WIDTH_MIN) self.lexicon_tree.setMaximumWidth(TREEWIDGET_WIDTH_MAX) self.lexicon_tree.setMinimumHeight(TREEWIDGET_HEIGHT_MIN) self.lexicon_tree.setHeaderLabel('') self.lexicon_tree.setItemsExpandable(True) # noinspection PyUnresolvedReferences self.lexicon_tree.itemClicked.connect(self.tree_item_clicked) def create_action(self, text=None, slot=None, tip=None, shortcut=None): """ This create actions for the File menu, things like Read Corpus, Rerun Corpus etc """ action = QAction(text, self) if shortcut: action.setShortcut(shortcut) if tip: action.setToolTip(tip) action.setStatusTip(tip) if slot: # noinspection PyUnresolvedReferences action.triggered.connect(slot) if shortcut: # noinspection PyUnresolvedReferences QShortcut(QKeySequence(shortcut), self).activated.connect(slot) return action def _get_filename_from_dialog(self, ftype='input'): self.determine_last_file() if self.last_file_path and self.last_file_type == ftype: open_dir = self.last_file_path else: open_dir = os.getcwd() # noinspection PyTypeChecker,PyCallByClass fname = QFileDialog.getOpenFileName(self, 'Select the {} file'.format(ftype), open_dir) process_all_gui_events() # HACK: fname is supposed to be a string (at least according to the # PyQt5 documentation), but for some reason fname is a tuple. # So we need this hack to make sure that fname is a string of a filename # -- Jackson Lee, 2015/06/22 # update: it's turned out that this behavior is due to compatibility # between PyQt and PySide. The "tuple" behavior is in line with the # newer API2 for PyQt. (PyQt on python 3 uses API2 by default.) # more here: http://srinikom.github.io/pyside-bz-archive/343.html # so perhaps we keep our current hack for now? # -- Jackson Lee, 2015/08/24 if fname and any(fname) and (type(fname) is tuple): return fname[0] else: # if this hack isn't needed somehow... return fname def corpus_dir_dialog(self): """ Pop up the "open a file" dialog and ask for which corpus text file to use """ self.corpus_filename = self._get_filename_from_dialog(ftype='corpus') process_all_gui_events() if type(self.corpus_filename) != str: return # note that self.corpus_filename is an absolute full path self.corpus_name = os.path.basename(self.corpus_filename) self.corpus_stem_name = Path(self.corpus_name).stem self.lexicon = read_corpus(self.corpus_filename) self.initialize_lexicon_tree() self.load_main_window(major_display=QWidget(), parameter_window=QWidget()) process_all_gui_events() self.status.clearMessage() self.status.showMessage( 'Corpus selected: {}'.format(self.corpus_filename)) def wordlist_dir_dialog(self): """ Pop up the "open a file" dialog and ask for which corpus text file to use """ self.corpus_filename = self._get_filename_from_dialog(ftype='wordlist') process_all_gui_events() if type(self.corpus_filename) != str: return # note that self.corpus_filename is an absolute full path self.corpus_name = os.path.basename(self.corpus_filename) self.corpus_stem_name = Path(self.corpus_name).stem self.lexicon = read_wordlist(self.corpus_filename) self.initialize_lexicon_tree() self.load_main_window(major_display=QWidget(), parameter_window=QWidget()) process_all_gui_events() self.status.clearMessage() self.status.showMessage( 'Wordlist selected: {}'.format(self.corpus_filename)) def parameters_dialog(self): if self.lexicon is None: warning = QMessageBox() warning.setIcon(QMessageBox.Warning) warning.setText('Parameters can only be accessed when an input ' 'file is specified.') warning.setWindowTitle('No input file selected') warning.setStandardButtons(QMessageBox.Ok) warning.exec_() return process_all_gui_events() parameters = self.lexicon.parameters() dialog = QDialog() layout = QVBoxLayout() layout.addWidget( QLabel('Filename: {}'.format(Path(self.corpus_filename).name))) file_type = 'Wordlist' if self.lexicon.file_is_wordlist else 'Corpus' layout.addWidget(QLabel('Type: {}'.format(file_type))) grid = QGridLayout() self.parameter_spinboxes = [QSpinBox() for _ in range(len(parameters))] for i, parameter_name in enumerate(sorted(parameters.keys())): self.parameter_spinboxes[i].setObjectName(parameter_name) self.parameter_spinboxes[i].setRange( *PARAMETERS_RANGES[parameter_name]) self.parameter_spinboxes[i].setValue(parameters[parameter_name]) self.parameter_spinboxes[i].setSingleStep(1) # noinspection PyUnresolvedReferences self.parameter_spinboxes[i].valueChanged.connect( self.update_parameter) grid.addWidget(QLabel(parameter_name), i, 0) grid.addWidget(self.parameter_spinboxes[i], i, 1) grid.addWidget(QLabel(PARAMETERS_HINTS[parameter_name]), i, 2) layout.addLayout(grid) reset_button = QPushButton() reset_button.setText('&Reset') # noinspection PyUnresolvedReferences reset_button.clicked.connect(self.reset_parameters) spacer = QWidget() # just for padding in tool_bar spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) tool_bar = QHBoxLayout() tool_bar.addWidget(spacer) # so that the buttons are right-aligned tool_bar.addWidget(reset_button) layout.addLayout(tool_bar) dialog.setLayout(layout) dialog.setWindowTitle('Parameters') dialog.exec_() def reset_parameters(self): self.lexicon.use_default_parameters() for i, (_, value) in \ enumerate(sorted(self.lexicon.parameters().items())): self.parameter_spinboxes[i].setValue(value) def update_parameter(self): for i in range(len(self.lexicon.parameters())): parameter_name, new_value = \ self.parameter_spinboxes[i].objectName(), \ self.parameter_spinboxes[i].value() self.lexicon.change_parameters(**{parameter_name: new_value}) def update_progress(self, progress_text, target_percentage): """ Update the progress dialog. This function is triggered by the "progress_signal" emitted from the linguistica component worker thread. """ self.progressDialog.setLabelText(progress_text) self.progressDialog.setValue(target_percentage) process_all_gui_events() # noinspection PyProtectedMember def run_file(self): if self.lexicon is None: warning = QMessageBox() warning.setIcon(QMessageBox.Warning) warning.setText('No input file is selected.') warning.setWindowTitle('Error') warning.setStandardButtons(QMessageBox.Ok) warning.exec_() return self.status.clearMessage() self.status.showMessage('Running the file {} now...' .format(self.corpus_name)) print('\nInput file in use:\n{}\n'.format(self.corpus_filename), flush=True) # set up the Linguistica components worker # The worker is a QThread. We spawn this thread, and the linguistica # components run on this new thread but not the main thread for the GUI. # This makes the GUI still responsive # while the long and heavy running process of # the Linguistica components is ongoing. self.lxa_worker = LinguisticaWorker(self.lexicon) self.lxa_worker.progress_signal.connect(self.update_progress) # set up progress dialog process_all_gui_events() self.progressDialog = QProgressDialog() self.progressDialog.setRange(0, 100) # it's like from 0% to 100% self.progressDialog.setLabelText('Initializing...') self.progressDialog.setValue(0) # initialize as 0 (= 0%) self.progressDialog.setWindowTitle( 'Processing {}'.format(self.corpus_name)) self.progressDialog.setCancelButton(None) self.progressDialog.resize(400, 100) process_all_gui_events() self.progressDialog.show() # We disable the "cancel" button # Setting up a "cancel" mechanism may not be a good idea, # since it would probably involve killing the linguistica component # worker at *any* point of its processing. # This may have undesirable effects (e.g., freezing the GUI) -- BAD! # make sure all GUI stuff up to this point has been processed before # doing the real work of running the Lxa components process_all_gui_events() # Now the real work begins here! self.lxa_worker.start() process_all_gui_events() self.lexicon = self.lxa_worker.get_lexicon() print('\nAll Linguistica components run for the file', flush=True) self.status.clearMessage() self.status.showMessage('{} processed'.format(self.corpus_name)) self.load_main_window(major_display=QWidget(), parameter_window=QWidget()) self.populate_lexicon_tree() self.update_last_file() process_all_gui_events() # display corpus name (in the tree header label) file_type = 'wordlist' if self.lexicon.file_is_wordlist else 'corpus' header_label = 'File: {}\nFile type: {}\n\n# word types: {:,}\n'.format( self.corpus_name, file_type, self.lexicon.number_of_word_types()) if file_type == 'corpus': header_label += '# word tokens: {:,}\n'.format( self.lexicon.number_of_word_tokens()) self.lexicon_tree.setHeaderLabel(header_label) @staticmethod def ensure_config_dir_exists(): if not os.path.isdir(CONFIG_DIR): os.mkdir(CONFIG_DIR) def determine_last_file(self): self.last_file_path = None self.last_file_type = None self.last_file_encoding = None if not os.path.isfile(CONFIG_LAST_FILE): return with open(CONFIG_LAST_FILE, encoding='utf8') as f: config_last_file = json.load(f) self.last_file_path = config_last_file['last_file_path'] self.last_file_type = config_last_file['last_file_type'] self.last_file_encoding = config_last_file['last_file_encoding'] def update_last_file(self): self.ensure_config_dir_exists() with open(CONFIG_LAST_FILE, 'w', encoding='utf8') as f: if self.lexicon.file_is_wordlist: file_type = 'wordlist' else: file_type = 'corpus' config = {'last_file_path': self.lexicon.file_abspath, 'last_file_type': file_type, 'last_file_encoding': self.lexicon.encoding, } json.dump(config, f) def populate_lexicon_tree(self): self.lexicon_tree.clear() process_all_gui_events() # wordlist ancestor = QTreeWidgetItem(self.lexicon_tree, [WORDLIST]) self.lexicon_tree.expandItem(ancestor) # word ngrams ancestor = QTreeWidgetItem(self.lexicon_tree, [WORD_NGRAMS]) self.lexicon_tree.expandItem(ancestor) for item_str in [BIGRAMS, TRIGRAMS]: item = QTreeWidgetItem(ancestor, [item_str]) self.lexicon_tree.expandItem(item) # signatures ancestor = QTreeWidgetItem(self.lexicon_tree, [SIGNATURES]) self.lexicon_tree.expandItem(ancestor) for item in [SIGS_TO_STEMS, WORDS_TO_SIGS]: self.lexicon_tree.expandItem(QTreeWidgetItem(ancestor, [item])) # tries ancestor = QTreeWidgetItem(self.lexicon_tree, [TRIES]) self.lexicon_tree.expandItem(ancestor) for item in [WORDS_AS_TRIES, SUCCESSORS, PREDECESSORS]: self.lexicon_tree.expandItem(QTreeWidgetItem(ancestor, [item])) # phonology ancestor = QTreeWidgetItem(self.lexicon_tree, [PHONOLOGY]) self.lexicon_tree.expandItem(ancestor) for item in [PHONES, BIPHONES, TRIPHONES]: self.lexicon_tree.expandItem(QTreeWidgetItem(ancestor, [item])) # manifolds ancestor = QTreeWidgetItem(self.lexicon_tree, [MANIFOLDS]) self.lexicon_tree.expandItem(ancestor) for item in [WORD_NEIGHBORS, VISUALIZED_GRAPH]: self.lexicon_tree.expandItem(QTreeWidgetItem(ancestor, [item])) self.status.clearMessage() self.status.showMessage('Navigation tree populated') print('Lexicon navigation tree populated', flush=True) def load_main_window(self, major_display=None, parameter_window=None): """ Refresh the main window for the updated display content (most probably after a click or some event is triggered) """ # get sizes of the three major PyQt objects major_display_size = self.majorDisplay.size() parameter_window_size = self.parameterWindow.size() lexicon_tree_size = self.lexicon_tree.size() if major_display: self.majorDisplay = major_display if parameter_window: self.parameterWindow = parameter_window # apply sizes to the major three objects self.majorDisplay.resize(major_display_size) self.parameterWindow.resize(parameter_window_size) self.lexicon_tree.resize(lexicon_tree_size) # set up: # 1) main splitter (b/w lexicon-tree+parameter window and major display) # 2) minor splitter (b/w lexicon-tree and parameter window) self.mainSplitter = QSplitter(Qt.Horizontal) self.mainSplitter.setHandleWidth(10) self.mainSplitter.setChildrenCollapsible(False) self.minorSplitter = QSplitter(Qt.Vertical) self.minorSplitter.setHandleWidth(10) self.minorSplitter.setChildrenCollapsible(False) self.minorSplitter.addWidget(self.lexicon_tree) self.minorSplitter.addWidget(self.parameterWindow) self.mainSplitter.addWidget(self.minorSplitter) self.mainSplitter.addWidget(self.majorDisplay) self.setCentralWidget(self.mainSplitter) def sig_to_stems_clicked(self, row): signature = self.sig_to_stems_major_table.item(row, 0).text() print(signature) signature = tuple(signature.split(SEP_SIG)) stems = sorted(self.lexicon.signatures_to_stems()[signature]) number_of_stems_per_column = 5 # create a master list of sublists, where each sublist contains k stems # k = number_of_stems_per_column stem_rows = list() stem_row = list() for i, stem in enumerate(stems, 1): stem_row.append(stem) if not i % number_of_stems_per_column: stem_rows.append(stem_row) stem_row = list() if stem_row: stem_rows.append(stem_row) # set up the minor table as table widget sig_to_stems_minor_table = QTableWidget() sig_to_stems_minor_table.horizontalHeader().hide() sig_to_stems_minor_table.verticalHeader().hide() sig_to_stems_minor_table.clear() sig_to_stems_minor_table.setRowCount(len(stem_rows)) sig_to_stems_minor_table.setColumnCount(number_of_stems_per_column) # fill in the minor table for row, stem_row in enumerate(stem_rows): for col, stem in enumerate(stem_row): item = QTableWidgetItem(stem) sig_to_stems_minor_table.setItem(row, col, item) sig_to_stems_minor_table.resizeColumnsToContents() minor_table_title = QLabel('{} (number of stems: {})' .format(SEP_SIG.join(signature), len(stems))) minor_table_widget_with_title = QWidget() layout = QVBoxLayout() layout.addWidget(minor_table_title) layout.addWidget(sig_to_stems_minor_table) minor_table_widget_with_title.setLayout(layout) new_display = QSplitter(Qt.Horizontal) new_display.setHandleWidth(10) new_display.setChildrenCollapsible(False) new_display.addWidget(self.sig_to_stems_major_table) new_display.addWidget(minor_table_widget_with_title) new_display_width = self.majorDisplay.width() / 2 new_display.setSizes( [new_display_width * 0.4, new_display_width * 0.6]) self.load_main_window(major_display=new_display) self.status.clearMessage() self.status.showMessage('{} selected'.format(signature)) def unavailable_for_wordlist(self): self.load_main_window(major_display=QWidget(), parameter_window=QWidget()) self.status.showMessage('') warning = QMessageBox() warning.setIcon(QMessageBox.Warning) warning.setText('Unavailable for a wordlist') warning.setWindowTitle('Error') warning.setStandardButtons(QMessageBox.Ok) warning.exec_() def tree_item_clicked(self, item): """ Trigger the appropriate action when something in the lexicon tree is clicked, and update the major display plus parameter window """ item_str = item.text(0) if item_str in {WORD_NGRAMS, SIGNATURES, TRIES, PHONOLOGY, MANIFOLDS}: return print('loading', item_str, flush=True) self.status.clearMessage() self.status.showMessage('Loading {}...'.format(item_str)) new_display = None new_parameter_window = None if item_str == WORDLIST: new_display = self.create_major_display_table( self.lexicon.word_phonology_dict().items(), key=lambda x: x[1].count, reverse=True, headers=['Word', 'Count', 'Frequency', 'Phones', 'Unigram plog', 'Avg unigram plog', 'Bigram plog', 'Avg bigram plog'], row_cell_functions=[ lambda x: x[0], lambda x: x[1].count, lambda x: x[1].frequency, lambda x: ' '.join(x[1].phones), lambda x: x[1].unigram_plog, lambda x: x[1].avg_unigram_plog, lambda x: x[1].bigram_plog, lambda x: x[1].avg_bigram_plog], cutoff=0) elif item_str == BIGRAMS: if self.lexicon.file_is_wordlist: self.unavailable_for_wordlist() return new_display = self.create_major_display_table( self.lexicon.word_bigram_counter().items(), key=lambda x: x[1], reverse=True, headers=['Bigram', 'Count'], row_cell_functions=[lambda x: SEP_NGRAM.join(x[0]), lambda x: x[1]], cutoff=2000) elif item_str == TRIGRAMS: if self.lexicon.file_is_wordlist: self.unavailable_for_wordlist() return new_display = self.create_major_display_table( self.lexicon.word_trigram_counter().items(), key=lambda x: x[1], reverse=True, headers=['Trigram', 'Count'], row_cell_functions=[lambda x: SEP_NGRAM.join(x[0]), lambda x: x[1]], cutoff=2000) elif item_str == SIGS_TO_STEMS: self.sig_to_stems_major_table = self.create_major_display_table( self.lexicon.signatures_to_stems().items(), key=lambda x: len(x[1]), reverse=True, headers=['Signature', 'Stem count', 'A few stems'], row_cell_functions=[lambda x: SEP_SIG.join(x[0]), lambda x: len(x[1]), lambda x: ', '.join(sorted(x[1])[:2]) + ', ...'], cutoff=0) # noinspection PyUnresolvedReferences self.sig_to_stems_major_table.cellClicked.connect( self.sig_to_stems_clicked) new_display = self.sig_to_stems_major_table elif item_str == WORDS_TO_SIGS: new_display = self.create_major_display_table( self.lexicon.words_to_signatures().items(), key=lambda x: len(x[1]), reverse=True, headers=['Word', 'Signature count', 'Signatures'], row_cell_functions=[lambda x: x[0], lambda x: len(x[1]), lambda x: ', '.join([SEP_SIG.join(sig) for sig in sorted(x[1])])], cutoff=2000) elif item_str == WORDS_AS_TRIES: words = self.lexicon.broken_words_left_to_right().keys() words_to_tries = dict() # key: word (str) # value: tuple of (str, str) # for left-to-right and right-to-left tries for word in words: l_r = ' '.join(self.lexicon.broken_words_left_to_right()[word]) r_l = ' '.join(self.lexicon.broken_words_right_to_left()[word]) words_to_tries[word] = (l_r, r_l) # left-right, right-left new_display = self.create_major_display_table( words_to_tries.items(), key=lambda x: x[0], reverse=False, headers=['Word', 'Reversed word', 'Left-to-right trie', 'Right-to-left trie'], row_cell_functions=[lambda x: x[0], lambda x: x[0][::-1], lambda x: x[1][0], lambda x: x[1][1]], cutoff=0, set_text_alignment=[(3, Qt.AlignRight)]) elif item_str == SUCCESSORS: new_display = self.create_major_display_table( self.lexicon.successors().items(), key=lambda x: len(x[1]), reverse=True, headers=['String', 'Successor count', 'Successors'], row_cell_functions=[lambda x: x[0], lambda x: len(x[1]), lambda x: ', '.join(sorted(x[1]))], cutoff=0) elif item_str == PREDECESSORS: new_display = self.create_major_display_table( self.lexicon.predecessors().items(), key=lambda x: len(x[1]), reverse=True, headers=['String', 'Predecessor count', 'Predecessors'], row_cell_functions=[lambda x: x[0], lambda x: len(x[1]), lambda x: ', '.join(sorted(x[1]))], cutoff=0) elif item_str == PHONES: new_display = self.create_major_display_table( self.lexicon.phone_dict().items(), key=lambda x: x[1].count, reverse=True, headers=['Phone', 'Count', 'Frequency', 'Plog'], row_cell_functions=[lambda x: x[0], lambda x: x[1].count, lambda x: x[1].frequency, lambda x: x[1].plog], cutoff=0) elif item_str == BIPHONES: new_display = self.create_major_display_table( self.lexicon.biphone_dict().items(), key=lambda x: x[1].count, reverse=True, headers=['Biphone', 'Count', 'Frequency', 'Mutual information (MI)', 'Weighted MI'], row_cell_functions=[lambda x: SEP_NGRAM.join(x[0]), lambda x: x[1].count, lambda x: x[1].frequency, lambda x: x[1].MI, lambda x: x[1].weighted_MI], cutoff=0) elif item_str == TRIPHONES: new_display = self.create_major_display_table( self.lexicon.phone_trigram_counter().items(), key=lambda x: x[1], reverse=True, headers=['Triphone', 'Count'], row_cell_functions=[lambda x: SEP_NGRAM.join(x[0]), lambda x: x[1]], cutoff=0) elif item_str == WORD_NEIGHBORS: if self.lexicon.file_is_wordlist: self.unavailable_for_wordlist() return word_to_freq = self.lexicon.word_unigram_counter() new_display = self.create_major_display_table( self.lexicon.words_to_neighbors().items(), key=lambda x: word_to_freq[x[0]], reverse=True, headers=['Word', 'Word count', 'Neighbors'], row_cell_functions=[lambda x: x[0], lambda x: word_to_freq[x[0]], lambda x: ' '.join(x[1])], cutoff=0) elif item_str == VISUALIZED_GRAPH: if self.lexicon.file_is_wordlist: self.unavailable_for_wordlist() return graph_width = self.screen_width - TREEWIDGET_WIDTH_MAX - 50 graph_height = self.screen_height - 70 html_name = 'show_manifold.html' manifold_name = '{}_manifold.json'.format(self.corpus_stem_name) manifold_filename = os.path.join(CONFIG_DIR, manifold_name) print('manifold_filename', manifold_filename) manifold_json_data = json_graph.node_link_data( self.lexicon.neighbor_graph()) json.dump(manifold_json_data, open(manifold_filename, 'w')) viz_html = os.path.join(CONFIG_DIR, html_name) print('viz_html', viz_html) # write the show_manifold html file with open(viz_html, 'w') as f: print(SHOW_MANIFOLD_HTML.format(os.path.dirname(__file__), graph_width, graph_height, manifold_filename), file=f) url = Path(viz_html).as_uri() print('url:', url) new_display = QWebView() new_display.setUrl(QUrl(url)) self.load_main_window(major_display=new_display, parameter_window=new_parameter_window) self.status.clearMessage() self.status.showMessage('{} selected'.format(item_str)) @staticmethod def create_major_display_table(input_iterable, key=lambda x: x, reverse=False, headers=None, row_cell_functions=None, cutoff=0, set_text_alignment=None): """ This is a general function for creating a tabular display for the major display. """ if not input_iterable: print('Warning: input is empty', flush=True) return if not hasattr(input_iterable, '__iter__'): print('Warning: input is not an iterable', flush=True) return number_of_headers = len(headers) number_of_columns = len(row_cell_functions) if number_of_headers != number_of_columns: print('headers and cell functions don\'t match', flush=True) return len_input = len(input_iterable) table_widget = QTableWidget() table_widget.clear() table_widget.setSortingEnabled(False) # set up row count if cutoff and cutoff < len_input: actual_cutoff = cutoff else: actual_cutoff = len_input table_widget.setRowCount(actual_cutoff) # set up column count and table headers table_widget.setColumnCount(number_of_headers) table_widget.setHorizontalHeaderLabels(headers) # fill in the table for row, x in enumerate(double_sorted(input_iterable, key=key, reverse=reverse)): for col, fn in enumerate(row_cell_functions): cell = fn(x) if isinstance(cell, (int, float)): # cell is numeric item = QTableWidgetItem() item.setData(Qt.EditRole, cell) else: # cell is not numeric item = QTableWidgetItem(cell) if set_text_alignment: for align_col, alignment in set_text_alignment: if col == align_col: item.setTextAlignment(alignment) table_widget.setItem(row, col, item) if not row < actual_cutoff: break table_widget.setSortingEnabled(True) table_widget.resizeColumnsToContents() return table_widget
class Sidebar(QWidget): def __init__(self): super().__init__() self.setWindowTitle = "Longway Sidebar" self.setGeometry(1920 - 180, 0, 180, 1080 - 32) self.color_gradient_left = QColor(100, 100, 100, 50) self.color_gradient_right = QColor(0, 0, 0, 255) self.layout = QVBoxLayout() self.layout.setSpacing(0) self.layout.setContentsMargins(0, 0, 0, 0) # self.layout.addWidget(QWidget()) self.setLayout(self.layout) # Splitter for storing tiles self.tiles = [] self.splitter = QSplitter(Qt.Vertical) self.splitter.setChildrenCollapsible(False) self.splitter.setMinimumHeight(20) self.splitter.setStyleSheet(''' QSplitter::handle:vertical { height: 1px; background: black }''') self.layout.addWidget(self.splitter) self.setWindowFlags(Qt.FramelessWindowHint) self.setAttribute(Qt.WA_TranslucentBackground) # Starting the show! self.addTile(TileSlideshow()) self.addTile(TileTest()) self.addTile(Tile()) self.addTile(Tile()) self.addTile(QWidget()) self.show() def paintEvent(self, event): # Initializing QPainter qp = QPainter() qp.begin(self) qp.setRenderHint(QPainter.Antialiasing) sidebar_rect = self.geometry() # Gradient gradient = QLinearGradient(0, 0, sidebar_rect.width(), 0) gradient.setColorAt(0.0, self.color_gradient_left) gradient.setColorAt(1.0, self.color_gradient_right) qp.setBrush(QBrush(gradient)) # qp.setPen(Qt.white) qp.drawRect(0, 0, sidebar_rect.width(), sidebar_rect.height()) # Glass highlight qp.setBrush(QBrush(Qt.white)) qp.setPen(QPen(QBrush(Qt.white), 0.01)) qp.setOpacity(0.1) qppath = QPainterPath() qppath.moveTo(sidebar_rect.width() * 0.2, 0) qppath.quadTo(sidebar_rect.width() * 0.3, sidebar_rect.height() * 0.5, sidebar_rect.width() * 0.2, sidebar_rect.height() - 1) qppath.lineTo(0, sidebar_rect.height()) qppath.lineTo(0, 0) qp.setClipPath(qppath) qp.drawRect(1, 1, sidebar_rect.width() - 1, sidebar.height() - 1) # Left border highlight qp.setOpacity(1.0) gradient = QLinearGradient(0, 0, 2, 0) gradient.setColorAt(0.0, QColor(255, 255, 255, 80)) gradient.setColorAt(1.0, QColor(0, 0, 0, 0)) qp.setBrush(QBrush(gradient)) # qp.setPen(Qt.transparent) qp.drawRect(0, 0, 8, sidebar_rect.height()) qp.end() def addTile(self, tile): self.tiles.append(tile) self.splitter.addWidget(self.tiles[-1])
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) sp = QSizePolicy() sp.setHorizontalPolicy(QSizePolicy.Expanding) sp.setVerticalPolicy(QSizePolicy.Expanding) sp.setVerticalStretch(1) wb_city_data = load_workbook(filename=r'DSTU-H_B_V.1.1-27_2010.xlsx', read_only=True, data_only=True) self.__ws_city_I_ser_m_sj = wb_city_data['Лист1'] self.__ws_city_t_z = wb_city_data['Т2. Температура зовн. пов.'] self.__cityChoice = QComboBox() self.__cityChoice.addItems(c[0].value for c in self.__ws_city_I_ser_m_sj['A6':'A605'] if c[0].value) self.__q_0Edit = KwNumberEdit(.5) self.__V_budEdit = KwNumberEdit(480) self.__NEdit = QSpinBox() self.__NEdit.setRange(1, 999) self.__NEdit.setSuffix(' чол') self.__NEdit.setValue(6) self.__t_gvEdit = QSpinBox() self.__t_gvEdit.setRange(25, 70) self.__t_gvEdit.setSingleStep(5) self.__t_gvEdit.setSuffix(' \N{DEGREE CELSIUS}') self.__t_gvEdit.setValue(45) self.__a_gvEdit = QSpinBox() self.__a_gvEdit.setRange(15, 99) self.__a_gvEdit.setSuffix(' л/добу') self.__a_gvEdit.setValue(35) self.__f_zEdit = KwNumberEdit(.8) self.__varthetaEdit = KwNumberEdit(1.7) self.__S_0Edit = KwNumberEdit(1.723) self.__F_rEdit = KwNumberEdit(1) self.__eta_0Edit = KwNumberEdit(0.813) self.__ULEdit = KwNumberEdit(4.6) self.__aEdit = KwNumberEdit(.007) self.__bEdit = KwNumberEdit(1.27E-5) self.__c_paEdit = KwNumberEdit(3.16) self.__P_tpEdit = KwNumberEdit(14.1) self.__epsilon_tpEdit = KwNumberEdit(5.5) self.__epsilon_elEdit = KwNumberEdit(.88) self.__P_elEdit = KwNumberEdit(2.6) self.__t_co_1Edit = QSpinBox() self.__t_co_1Edit.setRange(25, 70) self.__t_co_1Edit.setSingleStep(5) self.__t_co_1Edit.setSuffix(' \N{DEGREE CELSIUS}') self.__t_co_1Edit.setValue(35) self.__t_co_2Edit = QSpinBox() self.__t_co_2Edit.setRange(20, 60) self.__t_co_2Edit.setSingleStep(5) self.__t_co_2Edit.setSuffix(' \N{DEGREE CELSIUS}') self.__t_co_2Edit.setValue(30) self.__eta_KEdit = KwNumberEdit(.93) self.__Q_n_rEdit = KwNumberEdit(35600) self.__c_gazEdit = KwNumberEdit(.55) self.__c_elEdit = KwNumberEdit(.25) self.__q_gruEdit = KwNumberEdit(21) self.__d_gruEdit = QSpinBox() self.__d_gruEdit.setRange(5, 99) self.__d_gruEdit.setSuffix(' мм') self.__d_gruEdit.setValue(25) self.__l_0_gruEdit = KwNumberEdit(1.7) calcButton = QPushButton('Визначити') calcButton.setObjectName('calcButton') self.__outputConsoleBrowser = KwConsoleBrowser(setup={'font': {'name': QFont('Hack').family(), 'size': 8}, 'color': 'rgb(255, 255, 255)', 'background_color': 'rgba(0, 0, 0, 218)', 'line_wrap': False}) self.__outputConsoleBrowser.setSizePolicy(sp) graphWidget = QMainWindow() graphWidget.setMinimumWidth(380) self.__graphCanvas = self.__createFigureCanvas() self.__graphNavigationToolbar = NavigationToolbar(self.__graphCanvas, self) graphWidget.addToolBar(Qt.TopToolBarArea, self.__graphNavigationToolbar) graphWidget.setCentralWidget(self.__graphCanvas) cityLayout = QFormLayout() cityLayout.addRow('Місто:', self.__cityChoice) soGroup = QGroupBox('Для системи опалення (СО):') soInputLayout = QFormLayout(soGroup) soInputLayout.addRow('Питома потужність тепловтрат, q<sub>0</sub>, ' 'Вт/(м<sup>3</sup>\N{MIDDLE DOT}\N{DEGREE CELSIUS}):', self.__q_0Edit) soInputLayout.addRow("Об'єм будинку по зовнішніх обмірах, V<sub>буд</sub>, " "м<sup>3</sup>:", self.__V_budEdit) sgvGroup = QGroupBox(u'Для системи гарячого водопостачання (СГК):') sgvInputLayout = QFormLayout(sgvGroup) sgvInputLayout.addRow('Кількість мешканців у будинку, N:', self.__NEdit) sgvInputLayout.addRow('Температура гарячої води, t<sub>гв</sub>:', self.__t_gvEdit) sgvInputLayout.addRow('Добова витрата гарячої води на 1 особу, a<sub>гв</sub>:', self.__a_gvEdit) sgkGroup = QGroupBox('Для системи геліоколекторів (СГК):') sgkInputLayout = QFormLayout(sgkGroup) sgkInputLayout.addRow('Ступінь заміщення тепловтрат СГВ, f<sub>з</sub>:', self.__f_zEdit) sgkInputLayout.addRow('Параметр, \u03D1:', self.__varthetaEdit) sgkInputLayout.addRow('Площа 1-го геліоколектора, S<sub>0</sub>, м<sup>2</sup>:', self.__S_0Edit) sgkInputLayout.addRow('F<sub>r</sub>:', self.__F_rEdit) sgkInputLayout.addRow('Оптичний ККД, \N{GREEK SMALL LETTER ETA}:', self.__eta_0Edit) sgkInputLayout.addRow('Коефіцієнт тепловтрат, UL, Вт/(м<sup>2</sup>)' '\N{MIDDLE DOT}\N{DEGREE CELSIUS}):', self.__ULEdit) sgkInputLayout.addRow('a:', self.__aEdit) sgkInputLayout.addRow('b:', self.__bEdit) sgkInputLayout.addRow('c<sub>pa</sub>, кДж/(кг\N{MIDDLE DOT}\N{DEGREE CELSIUS}):', self.__c_paEdit) tpGroup = QGroupBox('Для теплової помпи (ТП):') tpInputLayout = QFormLayout(tpGroup) tpInputLayout.addRow('Теплова потужність, P<sub>тп</sub>, кВт:', self.__P_tpEdit) tpInputLayout.addRow('Тепловий к.к.д, \N{GREEK SMALL LETTER EPSILON}' '<sub>тп</sub>', self.__epsilon_tpEdit) tpInputLayout.addRow('Електричний к.к.д., \N{GREEK SMALL LETTER EPSILON}' '<sub>ел</sub>:', self.__epsilon_elEdit) tpInputLayout.addRow('Електрична потужність, P<sub>ел</sub>, кВт:', self.__P_elEdit) tpInputLayout.addRow('Т-ра нагрітої води для СО підлоги, t<sub>co 1</sub>:', self.__t_co_1Edit) tpInputLayout.addRow('Т-ра охолодженої води для СО підлоги, t<sub>co 2</sub>:', self.__t_co_2Edit) tpInputLayout.addRow('К.к.д. згоряння палива, eta_K:', self.__eta_KEdit) tpInputLayout.addRow('Нижча теплота згоряння палива, Q<sub>n r</sub>, кДж/м<sup>3</sup>:', self.__Q_n_rEdit) tpInputLayout.addRow('Вартість 1 м<sup>3</sup> газу, c<sub>газ</sub>, грн/м<sup>3</sup>:', self.__c_gazEdit) tpInputLayout.addRow('Вартість 1 кВт\N{MIDDLE DOT}год, c<sub>ел</sub>, ' 'грн/м<sup>3</sup>:', self.__c_elEdit) gruGroup = QGroupBox('Для ґрунту і контуру СО підлоги:') gruInputEdit = QFormLayout(gruGroup) gruInputEdit.addRow('Питома тепловіддача ґрунту, q<sub>ґр</sub>, ' 'Вт/м<sup>2</sup>:', self.__q_gruEdit) gruInputEdit.addRow('Внутрішній діаметр, d, мм:', self.__d_gruEdit) gruInputEdit.addRow('Питома довжина тепловідбору, l<sub>0</sub>, ' 'м/м<sup>2</sup>:', self.__l_0_gruEdit) inputScrollArea = QScrollArea() inputScrollArea.setWidgetResizable(True) inputScrollArea.setSizePolicy(sp) inputDataPanel = QWidget() inputDataLayout = QVBoxLayout(inputDataPanel) inputDataLayout.addLayout(cityLayout) inputDataLayout.addWidget(soGroup) inputDataLayout.addWidget(sgvGroup) inputDataLayout.addWidget(sgkGroup) inputDataLayout.addWidget(tpGroup) inputDataLayout.addWidget(gruGroup) inputScrollArea.setWidget(inputDataPanel) inputWidget = QWidget() inputLayout = QFormLayout(inputWidget) #inputLayout.setContentsMargins(0, 0, 0, 0) inputLayout.setRowWrapPolicy(QFormLayout.WrapAllRows) inputLayout.addRow('Вхідні дані:', inputScrollArea) inputLayout.addWidget(calcButton) consoleViewWidget = QWidget() consoleViewLayout = QFormLayout(consoleViewWidget) consoleViewLayout.setRowWrapPolicy(QFormLayout.WrapAllRows) consoleViewLayout.addRow(u'Результати:', self.__outputConsoleBrowser) contentSplitter = QSplitter(Qt.Horizontal) contentSplitter.setStretchFactor(0, 1) contentSplitter.setStretchFactor(1, 0) contentSplitter.setSizes([350, 380]) contentSplitter.setChildrenCollapsible(False) ioSplitter = QSplitter(Qt.Vertical) ioSplitter.setStretchFactor(0, 1) ioSplitter.setStretchFactor(1, 0) ioSplitter.setSizes([200, 320]) ioSplitter.setChildrenCollapsible(False) ioSplitter.setMinimumWidth(380) ioSplitter.addWidget(inputWidget) ioSplitter.addWidget(consoleViewWidget) contentSplitter.addWidget(ioSplitter) contentSplitter.addWidget(graphWidget) self.setCentralWidget(contentSplitter) self.resize(1200, 640) # <<< for inputValueWidget in inputWidget.findChildren((QAbstractSpinBox, KwNumberEdit)): inputValueWidget.valueChanged.connect(self._reset_output_data) for inputValueWidget in inputWidget.findChildren(QComboBox): inputValueWidget.activated.connect(self._reset_output_data) calcButton.clicked.connect(self.calc_script)
class BlockEditor(QWidget, MooseWidget): """ The complete editing widget for a Block. The input file will only change when "Apply changes" has been clicked. Until then all changes only live in the widgets. The exceptions to this are the "Clone" and "Remove" buttons which just sends out signals to let others do work. Signals: needBlockList(list): When the type of a parameter references blocks (for example it is a VariableName), this is used to update the available options since they can change based on what the user does. blockRenamed(object, str): A block has been renamed. This is emitted so that the BlockTree can update the name of the block. blockChanged(object): Apply has been clicked for this block. cloneBlock(object): The user wants to clone the block we are currently editing. removeBlock(object): The user wants to remove the block we are currently editing. editingFinished(object): The user is done editing this block. Typically done by closing the window. """ needBlockList = pyqtSignal(list) # list of paths that we need children for blockRenamed = pyqtSignal(object, str) # block with changes, old path blockChanged = pyqtSignal(object) # block that has changed cloneBlock = pyqtSignal(object) # block to clone removeBlock = pyqtSignal(object) # block to remove editingFinished = pyqtSignal() def __init__(self, block, type_to_block_map, **kwds): """ Sets up an editor for a block. Input: block[BlockInfo]: Block to be edited. """ super(BlockEditor, self).__init__(**kwds) self.block = block self.comment_edit = CommentEditor() self.comment_edit.textChanged.connect(self._blockChanged) self.splitter = None self.clone_button = None self.clone_shortcut = None self.remove_button = None self.apply_button = None self.reset_button = None self.new_parameter_button = None self.param_editor = None self.setWindowTitle(block.path) if block.types: self.param_editor = ParamsByType(block, type_to_block_map) elif block.parameters: self.param_editor = ParamsByGroup(block, block.orderedParameters(), type_to_block_map) else: self.param_editor = ParamsTable(block, block.orderedParameters(), type_to_block_map) self.param_editor.needBlockList.connect(self.needBlockList) self.param_editor.changed.connect(self._blockChanged) self.param_editor.blockRenamed.connect(self.blockRenamed) self._createButtons() self.applyChanges() self._current_commands = [] self._command_index = 0 self.user_params = [] self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Vertical) self.splitter.setChildrenCollapsible(False) self.splitter.addWidget(self.param_editor) self.splitter.addWidget(self.comment_edit) self.splitter.setStretchFactor(0,2) self.splitter.setStretchFactor(1,1) self.top_layout = WidgetUtils.addLayout(vertical=True) self.top_layout.addWidget(self.splitter) self.top_layout.addLayout(self.button_layout) self.setLayout(self.top_layout) self.setup() def _blockChanged(self, enabled=True): """ Sets the Apply and Reset buttons based on enabled. Input: enabled[bool]: Whether to set the buttons to enabled """ self.apply_button.setEnabled(enabled) self.reset_button.setEnabled(enabled) self.setWindowTitle(self.block.path) def setWatchedBlockList(self, path, children): self.param_editor.setWatchedBlockList(path, children) def _createButtons(self): """ Create allowable buttons for this Block. This will depend on whether this is a user added block. """ self.button_layout = WidgetUtils.addLayout() self.close_button = WidgetUtils.addButton(self.button_layout, self, "Apply && Close", self._applyAndClose) self.close_button.setToolTip("Apply any changes and close the window") self.apply_button = WidgetUtils.addButton(self.button_layout, self, "Apply", self.applyChanges) self.apply_button.setEnabled(False) self.apply_button.setToolTip("Apply changes made") self.reset_button = WidgetUtils.addButton(self.button_layout, self, "Reset", self.resetChanges) self.reset_button.setEnabled(False) self.reset_button.setToolTip("Reset changes to when this window was opened") self.new_parameter_button = WidgetUtils.addButton(self.button_layout, self, "Add parameter", self.addUserParamPressed) self.new_parameter_button.setToolTip("Add a non standard parameter") if self.block.user_added: self.clone_button = WidgetUtils.addButton(self.button_layout, self, "Clone Block", self._cloneBlock) self.clone_shortcut = WidgetUtils.addShortcut(self, "Ctrl+N", self._cloneBlock, shortcut_with_children=True) self.clone_button.setToolTip("Clone this block with the same parameters") self.remove_button = WidgetUtils.addButton(self.button_layout, self, "Remove Block", self._removeBlock) self.remove_button.setToolTip("Remove this block") def _findFreeParamName(self, max_params=1000): """ Find a free parameter name that can be safely added. Input: max_params[int]: Maximum number of tries before giving up. """ base = "NewParam" for i in range(max_params): param = '%s%s' % (base, i) if self.param_editor.paramValue(param) == None: return param def addUserParamPressed(self): """ The user wants to add a new user parameter to this block. """ new_name = self._findFreeParamName() self._blockChanged() self.param_editor.addUserParam(new_name) def _cloneBlock(self): """ The user wants to clone this block """ self.cloneBlock.emit(self.block) def _removeBlock(self): """ The user wants to remove this block. We ask to make sure they want to do this. """ button = QMessageBox.question(self, "Confirm remove", "Are you sure you want to delete %s" % self.block.path, QMessageBox.Yes, QMessageBox.No) if button == QMessageBox.Yes: self.removeBlock.emit(self.block) self.hide() self.editingFinished.emit() def applyChanges(self): """ Apply any changes the user has made. """ self.block.comments = self.comment_edit.getComments() self.param_editor.save() self.block.changed_by_user = True self._blockChanged(enabled=False) self.blockChanged.emit(self.block) def _applyAndClose(self): """ Apply any changes the user has made then close the window """ if self.apply_button.isEnabled(): self.applyChanges() self.close() def resetChanges(self): """ Reset any changes the user has made. """ self.comment_edit.setComments(self.block.comments) self.param_editor.reset() self._blockChanged(enabled=False) def updateWatchers(self): """ This should be called after creating a BlockEditor. This isn't called in the constructor because the caller will typically need to hook up the needBlockList signal first. """ self.param_editor.updateWatchers() def closeEvent(self, event): """ The user is done editing. """ self.editingFinished.emit()
class Listspace(QSplitter, ViewManager): """ Class implementing the listspace viewmanager class. @signal changeCaption(str) emitted if a change of the caption is necessary @signal editorChanged(str) emitted when the current editor has changed @signal editorChangedEd(Editor) emitted when the current editor has changed @signal lastEditorClosed() emitted after the last editor window was closed @signal editorOpened(str) emitted after an editor window was opened @signal editorOpenedEd(Editor) emitted after an editor window was opened @signal editorClosed(str) emitted just before an editor window gets closed @signal editorClosedEd(Editor) emitted just before an editor window gets closed @signal editorRenamed(str) emitted after an editor was renamed @signal editorRenamedEd(Editor) emitted after an editor was renamed @signal editorSaved(str) emitted after an editor window was saved @signal editorSavedEd(Editor) emitted after an editor window was saved @signal checkActions(Editor) emitted when some actions should be checked for their status @signal cursorChanged(Editor) emitted after the cursor position of the active window has changed @signal breakpointToggled(Editor) emitted when a breakpoint is toggled. @signal bookmarkToggled(Editor) emitted when a bookmark is toggled. @signal syntaxerrorToggled(Editor) emitted when a syntax error is toggled. @signal previewStateChanged(bool) emitted to signal a change in the preview state @signal editorLanguageChanged(Editor) emitted to signal a change of an editors language @signal editorTextChanged(Editor) emitted to signal a change of an editor's text @signal editorLineChanged(str,int) emitted to signal a change of an editor's current line (line is given one based) """ changeCaption = pyqtSignal(str) editorChanged = pyqtSignal(str) editorChangedEd = pyqtSignal(Editor) lastEditorClosed = pyqtSignal() editorOpened = pyqtSignal(str) editorOpenedEd = pyqtSignal(Editor) editorClosed = pyqtSignal(str) editorClosedEd = pyqtSignal(Editor) editorRenamed = pyqtSignal(str) editorRenamedEd = pyqtSignal(Editor) editorSaved = pyqtSignal(str) editorSavedEd = pyqtSignal(Editor) checkActions = pyqtSignal(Editor) cursorChanged = pyqtSignal(Editor) breakpointToggled = pyqtSignal(Editor) bookmarkToggled = pyqtSignal(Editor) syntaxerrorToggled = pyqtSignal(Editor) previewStateChanged = pyqtSignal(bool) editorLanguageChanged = pyqtSignal(Editor) editorTextChanged = pyqtSignal(Editor) editorLineChanged = pyqtSignal(str, int) def __init__(self, parent): """ Constructor @param parent parent widget (QWidget) """ self.stacks = [] QSplitter.__init__(self, parent) ViewManager.__init__(self) self.setChildrenCollapsible(False) self.viewlist = QListWidget(self) policy = self.viewlist.sizePolicy() policy.setHorizontalPolicy(QSizePolicy.Ignored) self.viewlist.setSizePolicy(policy) self.addWidget(self.viewlist) self.viewlist.setContextMenuPolicy(Qt.CustomContextMenu) self.viewlist.currentRowChanged.connect(self.__showSelectedView) self.viewlist.customContextMenuRequested.connect(self.__showMenu) self.stackArea = QSplitter(self) self.stackArea.setChildrenCollapsible(False) self.addWidget(self.stackArea) self.stackArea.setOrientation(Qt.Vertical) stack = StackedWidget(self.stackArea) self.stackArea.addWidget(stack) self.stacks.append(stack) self.currentStack = stack stack.currentChanged.connect(self.__currentChanged) stack.installEventFilter(self) self.setSizes([int(self.width() * 0.2), int(self.width() * 0.8)]) # 20% for viewlist, 80% for the editors self.__inRemoveView = False self.__initMenu() self.contextMenuEditor = None self.contextMenuIndex = -1 def __initMenu(self): """ Private method to initialize the viewlist context menu. """ self.__menu = QMenu(self) self.__menu.addAction( UI.PixmapCache.getIcon("tabClose.png"), self.tr('Close'), self.__contextMenuClose) self.closeOthersMenuAct = self.__menu.addAction( UI.PixmapCache.getIcon("tabCloseOther.png"), self.tr("Close Others"), self.__contextMenuCloseOthers) self.__menu.addAction( self.tr('Close All'), self.__contextMenuCloseAll) self.__menu.addSeparator() self.saveMenuAct = self.__menu.addAction( UI.PixmapCache.getIcon("fileSave.png"), self.tr('Save'), self.__contextMenuSave) self.__menu.addAction( UI.PixmapCache.getIcon("fileSaveAs.png"), self.tr('Save As...'), self.__contextMenuSaveAs) self.__menu.addAction( UI.PixmapCache.getIcon("fileSaveAll.png"), self.tr('Save All'), self.__contextMenuSaveAll) self.__menu.addSeparator() self.openRejectionsMenuAct = self.__menu.addAction( self.tr("Open 'rejection' file"), self.__contextMenuOpenRejections) self.__menu.addSeparator() self.__menu.addAction( UI.PixmapCache.getIcon("print.png"), self.tr('Print'), self.__contextMenuPrintFile) self.__menu.addSeparator() self.copyPathAct = self.__menu.addAction( self.tr("Copy Path to Clipboard"), self.__contextMenuCopyPathToClipboard) def __showMenu(self, point): """ Private slot to handle the customContextMenuRequested signal of the viewlist. @param point position to open the menu at (QPoint) """ if self.editors: itm = self.viewlist.itemAt(point) if itm is not None: row = self.viewlist.row(itm) self.contextMenuEditor = self.editors[row] self.contextMenuIndex = row if self.contextMenuEditor: self.saveMenuAct.setEnabled( self.contextMenuEditor.isModified()) fileName = self.contextMenuEditor.getFileName() self.copyPathAct.setEnabled(bool(fileName)) if fileName: rej = "{0}.rej".format(fileName) self.openRejectionsMenuAct.setEnabled( os.path.exists(rej)) else: self.openRejectionsMenuAct.setEnabled(False) self.closeOthersMenuAct.setEnabled( self.viewlist.count() > 1) self.__menu.popup(self.viewlist.mapToGlobal(point)) def canCascade(self): """ Public method to signal if cascading of managed windows is available. @return flag indicating cascading of windows is available """ return False def canTile(self): """ Public method to signal if tiling of managed windows is available. @return flag indicating tiling of windows is available """ return False def canSplit(self): """ public method to signal if splitting of the view is available. @return flag indicating splitting of the view is available. """ return True def tile(self): """ Public method to tile the managed windows. """ pass def cascade(self): """ Public method to cascade the managed windows. """ pass def _removeAllViews(self): """ Protected method to remove all views (i.e. windows). """ self.viewlist.clear() for win in self.editors: for stack in self.stacks: if stack.hasEditor(win): stack.removeWidget(win) break win.closeIt() def _removeView(self, win): """ Protected method to remove a view (i.e. window). @param win editor window to be removed """ self.__inRemoveView = True ind = self.editors.index(win) itm = self.viewlist.takeItem(ind) if itm: del itm for stack in self.stacks: if stack.hasEditor(win): stack.removeWidget(win) break win.closeIt() self.__inRemoveView = False if ind > 0: ind -= 1 else: if len(self.editors) > 1: ind = 1 else: return stack.setCurrentWidget(stack.firstEditor()) self._showView(self.editors[ind].parent()) aw = self.activeWindow() fn = aw and aw.getFileName() or None if fn: self.changeCaption.emit(fn) self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, aw.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(aw) def _addView(self, win, fn=None, noName="", next=False): """ Protected method to add a view (i.e. window). @param win editor assembly to be added @param fn filename of this editor (string) @param noName name to be used for an unnamed editor (string) @param next flag indicating to add the view next to the current view (bool) """ editor = win.getEditor() if fn is None: if not noName: self.untitledCount += 1 noName = self.tr("Untitled {0}").format(self.untitledCount) self.viewlist.addItem(noName) editor.setNoName(noName) else: txt = os.path.basename(fn) if not QFileInfo(fn).isWritable(): txt = self.tr("{0} (ro)").format(txt) itm = QListWidgetItem(txt) itm.setToolTip(fn) self.viewlist.addItem(itm) self.currentStack.addWidget(win) self.currentStack.setCurrentWidget(win) editor.captionChanged.connect(self.__captionChange) editor.cursorLineChanged.connect(self.__cursorLineChanged) index = self.editors.index(editor) self.viewlist.setCurrentRow(index) editor.setFocus() if fn: self.changeCaption.emit(fn) self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(editor) def __captionChange(self, cap, editor): """ Private method to handle caption change signals from the editor. Updates the listwidget text to reflect the new caption information. @param cap Caption for the editor (string) @param editor Editor to update the caption for """ fn = editor.getFileName() if fn: self.setEditorName(editor, fn) def __cursorLineChanged(self, lineno): """ Private slot to handle a change of the current editor's cursor line. @param lineno line number of the current editor's cursor (zero based) """ editor = self.sender() if editor: fn = editor.getFileName() if fn: self.editorLineChanged.emit(fn, lineno + 1) def _showView(self, win, fn=None): """ Protected method to show a view (i.e. window). @param win editor assembly to be shown @param fn filename of this editor (string) """ editor = win.getEditor() for stack in self.stacks: if stack.hasEditor(editor): stack.setCurrentWidget(win) self.currentStack = stack break index = self.editors.index(editor) self.viewlist.setCurrentRow(index) editor.setFocus() fn = editor.getFileName() if fn: self.changeCaption.emit(fn) self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(editor) def __showSelectedView(self, row): """ Private slot called to show a view selected in the list. @param row row number of the item clicked on (integer) """ if row != -1: self._showView(self.editors[row].parent()) self._checkActions(self.editors[row]) def activeWindow(self): """ Public method to return the active (i.e. current) window. @return reference to the active editor """ return self.currentStack.currentWidget() def showWindowMenu(self, windowMenu): """ Public method to set up the viewmanager part of the Window menu. @param windowMenu reference to the window menu """ pass def _initWindowActions(self): """ Protected method to define the user interface actions for window handling. """ pass def setEditorName(self, editor, newName): """ Public method to change the displayed name of the editor. @param editor editor window to be changed @param newName new name to be shown (string) """ if newName: currentRow = self.viewlist.currentRow() index = self.editors.index(editor) txt = os.path.basename(newName) if not QFileInfo(newName).isWritable(): txt = self.tr("{0} (ro)").format(txt) itm = self.viewlist.item(index) itm.setText(txt) itm.setToolTip(newName) self.viewlist.setCurrentRow(currentRow) self.changeCaption.emit(newName) def _modificationStatusChanged(self, m, editor): """ Protected slot to handle the modificationStatusChanged signal. @param m flag indicating the modification status (boolean) @param editor editor window changed """ currentRow = self.viewlist.currentRow() index = self.editors.index(editor) keys = [] if m: keys.append("fileModified.png") if editor.hasSyntaxErrors(): keys.append("syntaxError22.png") elif editor.hasWarnings(): keys.append("warning22.png") if not keys: keys.append("empty.png") self.viewlist.item(index).setIcon( UI.PixmapCache.getCombinedIcon(keys)) self.viewlist.setCurrentRow(currentRow) self._checkActions(editor) def _syntaxErrorToggled(self, editor): """ Protected slot to handle the syntaxerrorToggled signal. @param editor editor that sent the signal """ currentRow = self.viewlist.currentRow() index = self.editors.index(editor) keys = [] if editor.isModified(): keys.append("fileModified.png") if editor.hasSyntaxErrors(): keys.append("syntaxError22.png") elif editor.hasWarnings(): keys.append("warning22.png") if not keys: keys.append("empty.png") self.viewlist.item(index).setIcon( UI.PixmapCache.getCombinedIcon(keys)) self.viewlist.setCurrentRow(currentRow) ViewManager._syntaxErrorToggled(self, editor) def addSplit(self): """ Public method used to split the current view. """ stack = StackedWidget(self.stackArea) stack.show() self.stackArea.addWidget(stack) self.stacks.append(stack) self.currentStack = stack stack.currentChanged.connect(self.__currentChanged) stack.installEventFilter(self) if self.stackArea.orientation() == Qt.Horizontal: size = self.stackArea.width() else: size = self.stackArea.height() self.stackArea.setSizes( [int(size / len(self.stacks))] * len(self.stacks)) self.splitRemoveAct.setEnabled(True) self.nextSplitAct.setEnabled(True) self.prevSplitAct.setEnabled(True) def removeSplit(self): """ Public method used to remove the current split view. @return flag indicating successfull removal """ if len(self.stacks) > 1: stack = self.currentStack res = True savedEditors = stack.editors[:] for editor in savedEditors: res &= self.closeEditor(editor) if res: try: i = self.stacks.index(stack) except ValueError: return True if i == len(self.stacks) - 1: i -= 1 self.stacks.remove(stack) stack.close() self.currentStack = self.stacks[i] if len(self.stacks) == 1: self.splitRemoveAct.setEnabled(False) self.nextSplitAct.setEnabled(False) self.prevSplitAct.setEnabled(False) return True return False def getSplitOrientation(self): """ Public method to get the orientation of the split view. @return orientation of the split (Qt.Horizontal or Qt.Vertical) """ return self.stackArea.orientation() def setSplitOrientation(self, orientation): """ Public method used to set the orientation of the split view. @param orientation orientation of the split (Qt.Horizontal or Qt.Vertical) """ self.stackArea.setOrientation(orientation) def nextSplit(self): """ Public slot used to move to the next split. """ aw = self.activeWindow() _hasFocus = aw and aw.hasFocus() ind = self.stacks.index(self.currentStack) + 1 if ind == len(self.stacks): ind = 0 self.currentStack = self.stacks[ind] if _hasFocus: aw = self.activeWindow() if aw: aw.setFocus() index = self.editors.index(self.currentStack.currentWidget()) self.viewlist.setCurrentRow(index) def prevSplit(self): """ Public slot used to move to the previous split. """ aw = self.activeWindow() _hasFocus = aw and aw.hasFocus() ind = self.stacks.index(self.currentStack) - 1 if ind == -1: ind = len(self.stacks) - 1 self.currentStack = self.stacks[ind] if _hasFocus: aw = self.activeWindow() if aw: aw.setFocus() index = self.editors.index(self.currentStack.currentWidget()) self.viewlist.setCurrentRow(index) def __contextMenuClose(self): """ Private method to close the selected editor. """ if self.contextMenuEditor: self.closeEditorWindow(self.contextMenuEditor) def __contextMenuCloseOthers(self): """ Private method to close the other editors. """ index = self.contextMenuIndex for i in list(range(self.viewlist.count() - 1, index, -1)) + \ list(range(index - 1, -1, -1)): editor = self.editors[i] self.closeEditorWindow(editor) def __contextMenuCloseAll(self): """ Private method to close all editors. """ savedEditors = self.editors[:] for editor in savedEditors: self.closeEditorWindow(editor) def __contextMenuSave(self): """ Private method to save the selected editor. """ if self.contextMenuEditor: self.saveEditorEd(self.contextMenuEditor) def __contextMenuSaveAs(self): """ Private method to save the selected editor to a new file. """ if self.contextMenuEditor: self.saveAsEditorEd(self.contextMenuEditor) def __contextMenuSaveAll(self): """ Private method to save all editors. """ self.saveEditorsList(self.editors) def __contextMenuOpenRejections(self): """ Private slot to open a rejections file associated with the selected editor. """ if self.contextMenuEditor: fileName = self.contextMenuEditor.getFileName() if fileName: rej = "{0}.rej".format(fileName) if os.path.exists(rej): self.openSourceFile(rej) def __contextMenuPrintFile(self): """ Private method to print the selected editor. """ if self.contextMenuEditor: self.printEditor(self.contextMenuEditor) def __contextMenuCopyPathToClipboard(self): """ Private method to copy the file name of the selected editor to the clipboard. """ if self.contextMenuEditor: fn = self.contextMenuEditor.getFileName() if fn: cb = QApplication.clipboard() cb.setText(fn) def __currentChanged(self, index): """ Private slot to handle the currentChanged signal. @param index index of the current editor """ if index == -1 or not self.editors: return editor = self.activeWindow() if editor is None: return self._checkActions(editor) editor.setFocus() fn = editor.getFileName() if fn: self.changeCaption.emit(fn) if not self.__inRemoveView: self.editorChanged.emit(fn) self.editorLineChanged.emit( fn, editor.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(editor) cindex = self.editors.index(editor) self.viewlist.setCurrentRow(cindex) def eventFilter(self, watched, event): """ Public method called to filter the event queue. @param watched the QObject being watched @param event the event that occurred @return flag indicating, if we handled the event """ if event.type() == QEvent.MouseButtonPress and \ not event.button() == Qt.RightButton: switched = True if isinstance(watched, QStackedWidget): switched = watched is not self.currentStack self.currentStack = watched elif isinstance(watched, QScintilla.Editor.Editor): for stack in self.stacks: if stack.hasEditor(watched): switched = stack is not self.currentStack self.currentStack = stack break currentWidget = self.currentStack.currentWidget() if currentWidget: index = self.editors.index(currentWidget) self.viewlist.setCurrentRow(index) aw = self.activeWindow() if aw is not None: self._checkActions(aw) aw.setFocus() fn = aw.getFileName() if fn: self.changeCaption.emit(fn) if switched: self.editorChanged.emit(fn) self.editorLineChanged.emit( fn, aw.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(aw) return False
class BlockEditor(QWidget, MooseWidget): """ The complete editing widget for a Block. The input file will only change when "Apply changes" has been clicked. Until then all changes only live in the widgets. The exceptions to this are the "Clone" and "Remove" buttons which just sends out signals to let others do work. Signals: needBlockList(list): When the type of a parameter references blocks (for example it is a VariableName), this is used to update the available options since they can change based on what the user does. blockRenamed(object, str): A block has been renamed. This is emitted so that the BlockTree can update the name of the block. blockChanged(object): Apply has been clicked for this block. cloneBlock(object): The user wants to clone the block we are currently editing. removeBlock(object): The user wants to remove the block we are currently editing. editingFinished(object): The user is done editing this block. Typically done by closing the window. """ needBlockList = pyqtSignal(list) # list of paths that we need children for blockRenamed = pyqtSignal(object, str) # block with changes, old path blockChanged = pyqtSignal(object) # block that has changed cloneBlock = pyqtSignal(object) # block to clone removeBlock = pyqtSignal(object) # block to remove editingFinished = pyqtSignal() def __init__(self, block, type_to_block_map, **kwds): """ Sets up an editor for a block. Input: block[BlockInfo]: Block to be edited. """ super(BlockEditor, self).__init__(**kwds) self.block = block self.comment_edit = CommentEditor() self.comment_edit.textChanged.connect(self._blockChanged) self.splitter = None self.clone_button = None self.clone_shortcut = None self.remove_button = None self.apply_button = None self.reset_button = None self.new_parameter_button = None self.param_editor = None self.setWindowTitle(block.path) if block.types: self.param_editor = ParamsByType(block, type_to_block_map) elif block.parameters: self.param_editor = ParamsByGroup(block, block.orderedParameters(), type_to_block_map) else: self.param_editor = ParamsTable(block, block.orderedParameters(), type_to_block_map) self.param_editor.needBlockList.connect(self.needBlockList) self.param_editor.changed.connect(self._blockChanged) self.param_editor.blockRenamed.connect(self.blockRenamed) self._createButtons() self.applyChanges() self._current_commands = [] self._command_index = 0 self.user_params = [] self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Vertical) self.splitter.setChildrenCollapsible(False) self.splitter.addWidget(self.param_editor) self.splitter.addWidget(self.comment_edit) self.splitter.setStretchFactor(0,2) self.splitter.setStretchFactor(1,1) self.top_layout = WidgetUtils.addLayout(vertical=True) self.top_layout.addWidget(self.splitter) self.top_layout.addLayout(self.button_layout) self.setLayout(self.top_layout) self.setup() def _blockChanged(self, enabled=True): """ Sets the Apply and Reset buttons based on enabled. Input: enabled[bool]: Whether to set the buttons to enabled """ self.apply_button.setEnabled(enabled) self.reset_button.setEnabled(enabled) self.setWindowTitle(self.block.path) def setWatchedBlockList(self, path, children): self.param_editor.setWatchedBlockList(path, children) def _createButtons(self): """ Create allowable buttons for this Block. This will depend on whether this is a user added block. """ self.button_layout = WidgetUtils.addLayout() if self.block.user_added: self.clone_button = WidgetUtils.addButton(self.button_layout, self, "Clone Block", self._cloneBlock) self.clone_shortcut = WidgetUtils.addShortcut(self, "Ctrl+N", self._cloneBlock, shortcut_with_children=True) self.clone_button.setToolTip("Clone this block with the same parameters") self.remove_button = WidgetUtils.addButton(self.button_layout, self, "Remove Block", self._removeBlock) self.remove_button.setToolTip("Remove this block") self.apply_button = WidgetUtils.addButton(self.button_layout, self, "Apply", self.applyChanges) self.apply_button.setEnabled(False) self.apply_button.setToolTip("Apply changes made") self.reset_button = WidgetUtils.addButton(self.button_layout, self, "Reset", self.resetChanges) self.reset_button.setEnabled(False) self.reset_button.setToolTip("Reset changes to when this window was opened") self.new_parameter_button = WidgetUtils.addButton(self.button_layout, self, "Add parameter", self.addUserParamPressed) self.new_parameter_button.setToolTip("Add a non standard parameter") self.close_button = WidgetUtils.addButton(self.button_layout, self, "Close", self._applyAndClose) self.close_button.setToolTip("Apply any changes and close the window") def _findFreeParamName(self, max_params=1000): """ Find a free parameter name that can be safely added. Input: max_params[int]: Maximum number of tries before giving up. """ base = "NewParam" for i in range(max_params): param = '%s%s' % (base, i) if self.param_editor.paramValue(param) == None: return param def addUserParamPressed(self): """ The user wants to add a new user parameter to this block. """ new_name = self._findFreeParamName() self._blockChanged() self.param_editor.addUserParam(new_name) def _cloneBlock(self): """ The user wants to clone this block """ self.cloneBlock.emit(self.block) def _removeBlock(self): """ The user wants to remove this block. We ask to make sure they want to do this. """ button = QMessageBox.question(self, "Confirm remove", "Are you sure you want to delete %s" % self.block.path, QMessageBox.Yes, QMessageBox.No) if button == QMessageBox.Yes: self.removeBlock.emit(self.block) self.hide() self.editingFinished.emit() def applyChanges(self): """ Apply any changes the user has made. """ self.block.comments = self.comment_edit.getComments() self.param_editor.save() self._blockChanged(enabled=False) self.blockChanged.emit(self.block) def _applyAndClose(self): """ Apply any changes the user has made then close the window """ if self.apply_button.isEnabled(): self.applyChanges() self.close() def resetChanges(self): """ Reset any changes the user has made. """ self.comment_edit.setComments(self.block.comments) self.param_editor.reset() self._blockChanged(enabled=False) def updateWatchers(self): """ This should be called after creating a BlockEditor. This isn't called in the constructor because the caller will typically need to hook up the needBlockList signal first. """ self.param_editor.updateWatchers() def closeEvent(self, event): """ The user is done editing. """ self.editingFinished.emit()
def __init__(self, parent=None): super().__init__(parent) self.record = -1 self.inspected = None self.oob_update = False prefs = QSettings() prefs.beginGroup("/General") timeout = prefs.value("/Timeout") dark_mode = prefs.value("/DarkMode") prefs.endGroup() # Instantiate core objects self.timeout_timer = QTimer() self.timeout_timer.setTimerType(Qt.VeryCoarseTimer) self.timeout_timer.setInterval(timeout * 1000) self.timeout_timer.setSingleShot(True) self.timeout_timer.timeout.connect(self.update_temp_log) self.systems = ActionsWidget(LogSource.SYSTEM) self.systems.acted.connect(self.log_item) self.events = ActionsWidget(LogSource.EVENT) self.events.acted.connect(self.log_item) self.compass = Compass() self.compass_widget = QWidget() compass_layout = QHBoxLayout() self.compass_widget.setLayout(compass_layout) compass_layout.addWidget(self.compass) self.compass.angle_event.connect(self.log_item) self.exact_angle = ExactAngle() self.exact_angle_widget = QWidget() exact_angle_layout = QHBoxLayout() self.exact_angle_widget.setLayout(exact_angle_layout) exact_angle_layout.addWidget(self.exact_angle) self.exact_angle.btn_event.connect(self.reset_timer) self.exact_angle.angle_event.connect(self.log_item) tab_widget = QTabWidget() tab_bar = tab_widget.tabBar() tab_bar.setFont(QFont('Consolas', 12, 3)) tab_widget.addTab(self.compass_widget, "Compass") tab_widget.addTab(self.exact_angle_widget, "Precise Angle") tab_widget.setStyleSheet(""" QTabWidget::pane { border-top: 2px solid #C2C7CB; } /* Style the tab using the tab sub-control. Note that it reads QTabBar _not_ QTabWidget */ QTabBar::tab { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #E1E1E1, stop: 0.4 #DDDDDD, stop: 0.5 #D8D8D8, stop: 1.0 #D3D3D3); border: 2px solid #C4C4C3; border-bottom-color: #C2C7CB; /* same as the pane color */ border-top-left-radius: 4px; border-top-right-radius: 4px; min-width: 8ex; padding: 2px; color: black; } QTabBar::tab:selected, QTabBar::tab:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #fafafa, stop: 0.4 #f4f4f4, stop: 0.5 #e7e7e7, stop: 1.0 #fafafa); } QTabBar::tab:selected { border-color: #ff0000; border-bottom-color: #C2C7CB; /* same as pane color */ } QTabBar::tab:!selected { margin-top: 2px; /* make non-selected tabs look smaller */ } """) header_layout = QHBoxLayout() self.zulu_time_label = QLabel() self.assessor_label = QLabel() self.date_label = QLabel() self.dl_label = QLabel() self.mnemonic_label = QLabel() header_layout.addWidget(self.zulu_time_label) header_layout.addWidget(self.assessor_label) header_layout.addWidget(self.date_label) header_layout.addWidget(self.dl_label) header_layout.addWidget(self.mnemonic_label) res = QApplication.primaryScreen().size() w, h = res.width(), res.height() if w > 1920 or h > 1080: hdr_font = QFont("Consolas", 16, 2) end_font = QFont("Consolas", 32, 5) else: hdr_font = QFont("Consolas", 14, 2) end_font = QFont("Consolas", 28, 5) for index in range(header_layout.count()): widget = header_layout.itemAt(index).widget() widget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum ) widget.setFont(hdr_font) widget.setAlignment(Qt.AlignCenter) # Setup logging state machine self.init_log_sm() # Setup splitters actions_splitter = QSplitter( Qt.Horizontal, frameShape=QFrame.StyledPanel, frameShadow=QFrame.Plain ) actions_splitter.addWidget(self.systems) actions_splitter.addWidget(self.events) actions_splitter.addWidget(tab_widget) actions_splitter.setChildrenCollapsible(False) main_splitter = QSplitter( Qt.Vertical, frameShape=QFrame.StyledPanel, frameShadow=QFrame.Plain ) self.log_area = QTableWidget(0, 3) self.log_area.cellDoubleClicked.connect(self.entry_inspected) self.log_area.cellChanged.connect(self.entry_changed) self.log_area.setHorizontalHeaderLabels( ["Time", "System", "Events"] ) self.log_area.horizontalHeader().setStretchLastSection(True) self.set_dark_mode(dark_mode) end_msn_btn = QPushButton("END\r\nMISSION") end_msn_btn.clicked.connect(self.end_mission) end_msn_btn.setFont(end_font) end_msn_btn.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) end_msn_btn.setStyleSheet("background-color: red; color: white") bottom_layout = QGridLayout() bottom_widget = QWidget() bottom_widget.setLayout(bottom_layout) bottom_layout.addWidget(self.log_area, 0, 0, 1, 7) bottom_layout.addWidget(end_msn_btn, 0, 8, 1, 1) main_splitter.addWidget(actions_splitter) main_splitter.addWidget(bottom_widget) main_splitter.setChildrenCollapsible(False) handle_css = """ QSplitter::handle { background-image: url(:/imgs/dot_pattern.png); background-repeat: repeat-xy; background-color: none; border: 1px solid gray; } QSplitter::handle:pressed { background-image: url(:/imgs/pressed.png); } """ actions_splitter.setStyleSheet(handle_css) main_splitter.setStyleSheet(handle_css) # Finalize layout main_layout = QVBoxLayout() main_layout.addLayout(header_layout) main_layout.addWidget(main_splitter) self.setLayout(main_layout)
class POSM(QMainWindow): def __init__(self): super().__init__() self.setLocale(QLocale(QLocale.English)) self.initUI() self.setAttribute(Qt.WA_AlwaysShowToolTips) sizegrip = QtWidgets.QSizeGrip(self) self.layout.addWidget(sizegrip, 0, QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight) self.record = [None] recordAction = QAction(datetime.datetime.now().strftime('%H:%M:%S'), self) recordAction.triggered.connect(lambda: self.changeMap(0)) self.recordMenu.addAction(recordAction) def initUI(self): self.layout = QHBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.console = InformationalConsole(app) self.horSplitter = QSplitter(Qt.Horizontal) self.horSplitter.setChildrenCollapsible(False) self.editionSplitter = QSplitter(Qt.Vertical) self.editionSplitter.setChildrenCollapsible(False) self.queryUI = QueryUI() self.queryUI.setOnRequestChanged(self.changeCurrentMap) self.editionSplitter.addWidget(self.queryUI) self.queryWidget = QWidget() self.queryWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.queryWidget.setLayout(QVBoxLayout()) self.queryWidget.layout().setContentsMargins(0, 0, 0, 0) self.queryWidget.layout().setSpacing(0) self.queryHeader = QLabel("Query") self.queryHeader.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.queryHeader.setFixedHeight(self.queryHeader.sizeHint().height() + 10) self.queryHeader.setContentsMargins(5, 5, 0, 5) self.queryWidget.layout().addWidget(self.queryHeader) self.queryText = CodeEditor() self.qlHighlighter = OverpassQLHighlighter(self.queryText.document()) self.queryText.setReadOnly(True) self.queryWidget.layout().addWidget(self.queryText) self.editionSplitter.addWidget(self.queryWidget) self.horSplitter.addWidget(self.editionSplitter) self.emptyMapPage = QWebEnginePage() self.emptyMapPage.setHtml(EMPTY_HTML) self.manualModePage = QWebEnginePage() soup = bs4.BeautifulSoup(EMPTY_HTML, features="html.parser") js = soup.new_tag("script") js.string = (MANUAL_MODE_JS_SCRIPT % (str([]))) soup.append(js) self.manualModePage.setHtml(str(soup)) self.mapRenderer = QWebEngineView() self.mapRenderer.setMinimumWidth(500) self.mapRenderer.setPage(self.emptyMapPage) self.consoleSplitter = QSplitter(Qt.Vertical) self.consoleSplitter.setChildrenCollapsible(False) self.consoleSplitter.addWidget(self.mapRenderer) self.consoleWidget = QWidget() self.consoleWidget.setLayout(QVBoxLayout()) self.consoleWidget.layout().setContentsMargins(0, 0, 0, 0) self.consoleWidget.layout().setSpacing(0) self.consoleHeader = QLabel("Console") self.consoleHeader.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.consoleHeader.setContentsMargins(5, 5, 0, 5) self.consoleWidget.layout().addWidget(self.consoleHeader) self.consoleWidget.layout().addWidget(self.console) self.consoleSplitter.addWidget(self.consoleWidget) self.horSplitter.addWidget(self.consoleSplitter) self.layout.addWidget(self.horSplitter) self.initMenuBar() centralWidget = QWidget(self) centralWidget.setLayout(self.layout) self.setCentralWidget(centralWidget) self.setWindowTitle('Python Open Street Map') def initMenuBar(self): menubar = self.menuBar() fileMenu = menubar.addMenu('File') openAct = QAction('Open netedit', self) openAct.triggered.connect(self.openNet) fileMenu.addAction(openAct) saveMenu = fileMenu.addMenu("Save") saveOutputAct = QAction('output', self) saveOutputAct.triggered.connect(self.saveNet) saveOutputAct.setShortcut('Ctrl+S') saveMenu.addAction(saveOutputAct) saveQueryAct = QAction('query', self) saveQueryAct.triggered.connect(self.saveQuery) saveQueryAct.setShortcut('Ctrl+Shift+S') saveMenu.addAction(saveQueryAct) saveInteractiveModeAct = QAction('interactive mode', self) saveInteractiveModeAct.triggered.connect(self.saveInteractiveQuery) saveMenu.addAction(saveInteractiveModeAct) openMenu = fileMenu.addMenu("Open") openQuery = QAction('query', self) openQuery.triggered.connect(self.openQuery) openQuery.setShortcut('Ctrl+O') openMenu.addAction(openQuery) openInteractiveMode = QAction('interactive mode', self) openInteractiveMode.triggered.connect(self.openInteractiveQuery) openMenu.addAction(openInteractiveMode) self.recordMenu = openMenu.addMenu("record") runMenu = menubar.addMenu('Run') playAct = QAction('Play', self) playAct.triggered.connect(self.playQuery) playAct.setShortcut('Ctrl+P') runMenu.addAction(playAct) playTableRowAct = QAction('Play row selection', self) playTableRowAct.triggered.connect(self.playTableRow) playTableRowAct.setShortcut('Ctrl+T') runMenu.addAction(playTableRowAct) self.requestMenu = menubar.addMenu('Request') addRequestAct = QAction('Add request', self) addRequestAct.triggered.connect(lambda b: self.addRequest()) addRequestAct.setShortcut('Ctrl+A') self.requestMenu.addAction(addRequestAct) templatesMenu = self.requestMenu.addMenu("Add template") addRoadAct = QAction('Roads', self) addRoadAct.triggered.connect(lambda: self.addTemplate([ OverpassFilter("highway", TagComparison.EQUAL, "", False, True), OverpassFilter("name", TagComparison.EQUAL, "", False, True), OverpassFilter("ref", TagComparison.EQUAL, "", False, True), OverpassFilter("maxspeed", TagComparison.AT_MOST, "120", False, False), OverpassFilter("lanes", TagComparison.EQUAL, "", False, True), OverpassFilter("oneway", TagComparison.EQUAL, "", False, True) ])) templatesMenu.addAction(addRoadAct) addMainRoadAct = QAction('Main roads', self) mainHighways = "^(motorway|trunk|primary|secondary|residential)(_link)?$" everythingButYes = "^(y(e([^s]|$|s.)|[^e]|$)|[^y]|$).*" addMainRoadAct.triggered.connect(lambda: self.addTemplate([ OverpassFilter("highway", TagComparison.EQUAL, mainHighways, False, False), OverpassFilter("construction", TagComparison.HAS_NOT_KEY, "", False, False), OverpassFilter("noexit", TagComparison.EQUAL, "yes", True, True), OverpassFilter("access", TagComparison.EQUAL, everythingButYes, True, False) ])) templatesMenu.addAction(addMainRoadAct) addParkingAct = QAction('Parking', self) addParkingAct.triggered.connect(lambda: self.addTemplate([ OverpassFilter("service", TagComparison.EQUAL, "parking", False, False), OverpassFilter("highway", TagComparison.HAS_KEY, "", False, True) ])) templatesMenu.addAction(addParkingAct) addPedestriansAct = QAction('Pedestrians', self) pedestrianHighway = [ "pedestrian", "footway", "path", "cycleway", "bridleway", "steps", "crossing" ] addPedestriansAct.triggered.connect(lambda: self.addTemplate([ OverpassFilter("highway", TagComparison.IS_ONE_OF, pedestrianHighway, False, True) ])) templatesMenu.addAction(addPedestriansAct) removeRequestAct = QAction('Remove current request', self) removeRequestAct.triggered.connect(self.removeRequest) removeRequestAct.setShortcut('Ctrl+R') self.requestMenu.addAction(removeRequestAct) self.manualModeAct = QAction( 'Switch between interactive and manual mode', self) self.manualModeAct.triggered.connect(self.switchManualMode) self.requestMenu.addAction(self.manualModeAct) self.manualModeMenu = menubar.addMenu('Manual mode') self.manualModeMenu.setEnabled(False) manualModeCleanPolygonAct = QAction('Clean polygon', self) manualModeCleanPolygonAct.triggered.connect( self.cleanManualModePolygon) self.manualModeMenu.addAction(manualModeCleanPolygonAct) manualModeGetPolygonAct = QAction('Polygon coordinates', self) manualModeGetPolygonAct.triggered.connect( lambda: self.manualModePage.runJavaScript( "getPolygons();", self.logManualModePolygonCoords)) self.manualModeMenu.addAction(manualModeGetPolygonAct) windowsMenu = menubar.addMenu('Windows') cleanMapAct = QAction('Clean map', self) cleanMapAct.triggered.connect(self.cleanMap) windowsMenu.addAction(cleanMapAct) self.showHideInteractiveModeAct = QAction('Interactive mode', self) self.showHideInteractiveModeAct.triggered.connect( self.showHideInteractiveMode) windowsMenu.addAction(self.showHideInteractiveModeAct) showHideConsole = QAction('Console', self) showHideConsole.triggered.connect(self.showHideConsole) windowsMenu.addAction(showHideConsole) showHideQuery = QAction('Query', self) showHideQuery.triggered.connect(self.showHideQuery) windowsMenu.addAction(showHideQuery) # ACTIONS def cleanMap(self): if self.queryText.isReadOnly(): if self.queryUI.getCurrentMap() is not None: self.mapRenderer.setPage(self.queryUI.updateMaps(EMPTY_HTML)) else: soup = bs4.BeautifulSoup(EMPTY_HTML, features="html.parser") js = soup.new_tag("script") js.string = (MANUAL_MODE_JS_SCRIPT % (str([]))) soup.append(js) self.manualModePage.setHtml(str(soup)) logging.info("Cleaning map") def changeMap(self, i): if i == 0: if not self.queryText.isReadOnly(): self.switchManualMode() if self.queryText.isReadOnly(): self.queryUI.reset() self.queryText.clear() self.mapRenderer.setPage(self.emptyMapPage) self.queryUI.updateMaps(EMPTY_HTML) elif self.record[i]["interactiveMode"]: if not self.queryText.isReadOnly(): self.switchManualMode() if self.queryText.isReadOnly(): self.queryUI.setQuery(self.record[i]["query"]) self.queryText.setPlainText(self.record[i]["query"].getQL()) self.mapRenderer.setPage( self.queryUI.updateMaps(self.record[i]["html"])) else: if self.queryText.isReadOnly(): self.switchManualMode() if not self.queryText.isReadOnly(): self.queryUI.reset() self.queryText.setPlainText(self.record[i]["query"]) self.manualModePage.setHtml(self.record[i]["html"]) self.mapRenderer.setPage(self.manualModePage) def logManualModePolygonCoords(self, coords): coordsString = " ".join([str(c) for point in coords for c in point]) logging.info("Polygon coordinates:\"{}\"".format(coordsString)) pyperclip.copy(coordsString) logging.debug("LINE") def cleanManualModePolygon(self): logging.info("Cleaning polygon.") self.manualModePage.runJavaScript( "cleanPolygon();", lambda returnValue: logging.debug("LINE")) def showHideInteractiveMode(self): if self.queryUI.isHidden(): if self.editionSplitter.isHidden(): self.editionSplitter.show() self.queryText.hide() self.queryUI.show() logging.info("Showing 'Interactive mode' window.") else: if self.queryText.isHidden(): self.editionSplitter.hide() self.queryUI.hide() logging.info("Hiding 'Interactive mode' window.") logging.debug("LINE") def showHideConsole(self): if self.console.isHidden(): self.console.show() logging.info("Showing 'Console' window.") self.consoleWidget.setMaximumHeight(QWIDGETSIZE_MAX) else: self.console.hide() self.consoleWidget.setMaximumHeight( self.queryHeader.sizeHint().height()) logging.info("Hiding 'Console' window.") logging.debug("LINE") def showHideQuery(self): if self.queryText.isHidden(): if self.editionSplitter.isHidden(): self.editionSplitter.show() self.queryUI.hide() self.queryText.show() logging.info("Showing 'Query' window.") self.queryWidget.setMaximumHeight(QWIDGETSIZE_MAX) else: if self.queryUI.isHidden(): self.editionSplitter.hide() self.queryText.hide() self.queryWidget.setMaximumHeight( self.queryHeader.sizeHint().height()) logging.info("Hiding 'Query' window.") logging.debug("LINE") def switchManualMode(self): if self.queryText.isReadOnly(): reply = QMessageBox.question( self, "Manual mode", "Are you sure?\nThe interactive mode will remain as it is now." ) if reply == QMessageBox.Yes: self.queryText.setReadOnly(False) self.queryUI.hide() for action in self.requestMenu.actions(): action.setEnabled(False) self.manualModeAct.setEnabled(True) self.manualModeMenu.setEnabled(True) self.showHideInteractiveModeAct.setEnabled(False) self.mapRenderer.setPage(self.manualModePage) logging.info("Switching to manual mode.") else: logging.info( "'Switch between interactive and manual mode' cancelled.") else: reply = QMessageBox.question( self, "Interactive mode", "Are you sure?\nThe current query will be removed.") if reply == QMessageBox.Yes: try: self.queryText.clear() self.queryText.setPlainText( self.queryUI.getQuery().getQL()) except BadFilterAttributes as e: logging.error(str(e)) except RuntimeError: logging.warning("Failed to write query.") self.queryText.clear() self.queryText.setPlainText("") self.queryText.setReadOnly(True) self.queryUI.show() for action in self.requestMenu.actions(): action.setEnabled(True) self.manualModeMenu.setEnabled(False) self.showHideInteractiveModeAct.setEnabled(True) self.changeCurrentMap(0) logging.info("Switching to interactive mode.") else: logging.info( "'Switch between interactive and manual mode' cancelled.") logging.info("Showing 'manual mode' polygon.") def addRequest(self, filters=None): self.queryUI.addRequestByFilters(filters) logging.info("Request added.") logging.debug("LINE") def addTemplate(self, filters): logging.info("Template applied.") self.queryUI.addRequestByFilters(filters) def removeRequest(self): reply = QMessageBox.question( self, "Remove current request", "Are you sure? This option is not undoable.") if reply == QMessageBox.Yes: self.queryUI.removeRequest() logging.info("'Remove request' successfully executed.") else: logging.info("'Remove request' cancelled.") logging.debug("LINE") def saveQuery(self): filename, selectedFilter = QFileDialog.getSaveFileName( self, 'Save query', expanduser("~/filename.txt"), "Text files (*.txt)") if filename != "": if self.queryText.isReadOnly(): try: query = self.queryUI.getQuery().getQL() f = open(filename, "w+") f.seek(0) f.truncate() f.write(query) f.close() logging.info("Query saved successfully.") except (RuntimeError, BadFilterAttributes) as e: logging.error(str(e)) except OSError: logging.error( "There was a problem creating the file with the query." ) else: try: f = open(filename, "w+") f.seek(0) f.truncate() f.write(self.queryText.toPlainText()) f.close() logging.info("Query saved successfully.") except OSError: logging.error( "There was a problem creating the file with the query." ) else: logging.info("\"Save query\" canceled.") logging.debug("LINE") def openQuery(self): filename, selectedFilter = QFileDialog.getOpenFileName( self, 'Open query', expanduser("~/filename.txt")) if filename != "": try: if self.queryText.isReadOnly(): self.switchManualMode() f = open(filename, "r") self.queryText.clear() self.queryText.setPlainText(f.read()) f.close() logging.info("File read successfully.") except UnicodeDecodeError: logging.error("The given file is not readable as text.") except OSError: logging.error("There was a problem opening the query file.") else: logging.info("\"Open query\" canceled.") logging.debug("LINE") def saveInteractiveQuery(self): filename, selectedFilter = QFileDialog.getSaveFileName( self, 'Save query', expanduser("~/filename.json"), "JSON files (*.json)") if filename != "": try: query = self.queryUI.getQuery() query.saveToFile(filename) logging.info("Query saved successfully.") except (RuntimeError, BadFilterAttributes) as e: logging.error(str(e)) except OSError: logging.error( "There was a problem creating the file with the query.") else: logging.info("\"Save query\" canceled.") logging.debug("LINE") def openInteractiveQuery(self): filename, selectedFilter = QFileDialog.getOpenFileName( self, 'Open query', expanduser("~/filename.json")) if filename != "": try: self.queryUI.setQuery(OverpassQuery.getFromFile(filename)) if not self.queryText.isReadOnly(): self.switchManualMode() except json.decoder.JSONDecodeError: logging.error( "The given file has not the right format (json). The file could not be opened." ) except UnicodeDecodeError: logging.error( "The given file is not readable as text. The file could not be opened." ) except (TypeError, KeyError): logging.error( "Fields are missing from the file or there are fields with the wrong data type. " "The file could not be opened.") except OSError: logging.error( "There was a problem opening the query file. The file could not be opened." ) else: logging.info("\"Open query\" canceled.") logging.debug("LINE") def saveNet(self): filename, selectedFilter = QFileDialog.getSaveFileName( self, 'Save File', expanduser("~/filenameWithoutExtension")) if filename != "": buildNet(filename) else: logging.info("\"Save File\" canceled.") logging.debug("LINE") return filename def openNet(self): try: filename = self.saveNet() if filename == "": logging.error("Can't open NETEDIT without a file.") else: openNetedit(filename + ".net.xml") logging.info("Opening NETEDIT.") logging.warning( "If NETEDIT is not open in ten seconds, there was an unhandled problem." ) logging.debug("LINE") except OSError: logging.error("Can't find NETEDIT.") except Exception: logging.error(traceback.format_exc()) # POLYGONS def changeCurrentMap(self, i): if self.queryUI.getCurrentMap() is None: self.mapRenderer.setPage(self.emptyMapPage) else: self.mapRenderer.setPage(self.queryUI.getCurrentMap()) def playQuery(self): newRecord = { "interactiveMode": self.queryText.isReadOnly(), "query": self.queryText.toPlainText(), "html": "" } if self.queryText.isReadOnly(): try: query = self.queryUI.getQuery() newRecord["query"] = query self.queryText.setPlainText(query.getQL()) except (RuntimeError, BadFilterAttributes) as e: logging.error(str(e)) return try: html = buildHTMLWithQuery(self.queryText.toPlainText()) if self.queryText.isReadOnly(): self.mapRenderer.setPage(self.queryUI.updateMaps(html)) newRecord["html"] = html else: soup = bs4.BeautifulSoup(html, features="html.parser") js = soup.new_tag("script") js.string = (MANUAL_MODE_JS_SCRIPT % (str([]))) soup.append(js) self.manualModePage.setHtml(str(soup)) newRecord["html"] = str(soup) self.mapRenderer.setPage(self.manualModePage) logging.info("Query drawn.") logging.debug("LINE") self.addRecord(newRecord) except (OverpassRequestException, OsmnxException) as e: logging.error(str(e)) except ox.EmptyOverpassResponse: logging.error("There are no elements with the given query.") except OSError: logging.error( "There was a problem creating the file with the request response." ) except Exception: logging.error(traceback.format_exc()) def addRecord(self, newRecord): self.record.append(newRecord) index = len(self.record) - 1 recordAction = QAction(datetime.datetime.now().strftime('%H:%M:%S'), self) recordAction.triggered.connect(lambda: self.changeMap(index)) self.recordMenu.addAction(recordAction) def playTableRow(self): try: self.mapRenderer.setPage(self.queryUI.updateMapFromRow()) except (OverpassRequestException, OsmnxException) as e: logging.error(str(e)) logging.warning( "Before open NETEDIT you must run a query with the row filters applied." ) except ox.EmptyOverpassResponse: logging.error("There are no elements with the given row.") except OSError: logging.error( "There was a problem creating the file with the row selection." ) except RuntimeError as e: logging.error(str(e)) except Exception: logging.error(traceback.format_exc()) logging.debug("LINE") # EVENTS def closeEvent(self, event): for f in os.listdir(tempDir): os.remove(os.path.join(tempDir, f)) QMainWindow.closeEvent(self, event)
class Tabview(ViewManager): """ Class implementing a tabbed viewmanager class embedded in a splitter. @signal changeCaption(str) emitted if a change of the caption is necessary @signal editorChanged(str) emitted when the current editor has changed @signal editorChangedEd(Editor) emitted when the current editor has changed @signal lastEditorClosed() emitted after the last editor window was closed @signal editorOpened(str) emitted after an editor window was opened @signal editorOpenedEd(Editor) emitted after an editor window was opened @signal editorClosed(str) emitted just before an editor window gets closed @signal editorClosedEd(Editor) emitted just before an editor window gets closed @signal editorRenamed(str) emitted after an editor was renamed @signal editorRenamedEd(Editor) emitted after an editor was renamed @signal editorSaved(str) emitted after an editor window was saved @signal editorSavedEd(Editor) emitted after an editor window was saved @signal checkActions(Editor) emitted when some actions should be checked for their status @signal cursorChanged(Editor) emitted after the cursor position of the active window has changed @signal breakpointToggled(Editor) emitted when a breakpoint is toggled. @signal bookmarkToggled(Editor) emitted when a bookmark is toggled. @signal syntaxerrorToggled(Editor) emitted when a syntax error is toggled. @signal previewStateChanged(bool) emitted to signal a change in the preview state @signal editorLanguageChanged(Editor) emitted to signal a change of an editors language @signal editorTextChanged(Editor) emitted to signal a change of an editor's text @signal editorLineChanged(str,int) emitted to signal a change of an editor's current line (line is given one based) """ changeCaption = pyqtSignal(str) editorChanged = pyqtSignal(str) editorChangedEd = pyqtSignal(Editor) lastEditorClosed = pyqtSignal() editorOpened = pyqtSignal(str) editorOpenedEd = pyqtSignal(Editor) editorClosed = pyqtSignal(str) editorClosedEd = pyqtSignal(Editor) editorRenamed = pyqtSignal(str) editorRenamedEd = pyqtSignal(Editor) editorSaved = pyqtSignal(str) editorSavedEd = pyqtSignal(Editor) checkActions = pyqtSignal(Editor) cursorChanged = pyqtSignal(Editor) breakpointToggled = pyqtSignal(Editor) bookmarkToggled = pyqtSignal(Editor) syntaxerrorToggled = pyqtSignal(Editor) previewStateChanged = pyqtSignal(bool) editorLanguageChanged = pyqtSignal(Editor) editorTextChanged = pyqtSignal(Editor) editorLineChanged = pyqtSignal(str, int) def __init__(self, parent): """ Constructor @param parent parent widget (QWidget) """ self.tabWidgets = [] self.__splitter = QSplitter(parent) ViewManager.__init__(self) self.__splitter.setChildrenCollapsible(False) tw = TabWidget(self) self.__splitter.addWidget(tw) self.tabWidgets.append(tw) self.currentTabWidget = tw self.currentTabWidget.showIndicator(True) tw.currentChanged.connect(self.__currentChanged) tw.installEventFilter(self) tw.tabBar().installEventFilter(self) self.__splitter.setOrientation(Qt.Vertical) self.__inRemoveView = False self.maxFileNameChars = Preferences.getUI( "TabViewManagerFilenameLength") self.filenameOnly = Preferences.getUI("TabViewManagerFilenameOnly") def mainWidget(self): """ Public method to return a reference to the main Widget of a specific view manager subclass. @return reference to the main widget @rtype QWidget """ return self.__splitter def canCascade(self): """ Public method to signal if cascading of managed windows is available. @return flag indicating cascading of windows is available """ return False def canTile(self): """ Public method to signal if tiling of managed windows is available. @return flag indicating tiling of windows is available """ return False def canSplit(self): """ public method to signal if splitting of the view is available. @return flag indicating splitting of the view is available. """ return True def tile(self): """ Public method to tile the managed windows. """ pass def cascade(self): """ Public method to cascade the managed windows. """ pass def _removeAllViews(self): """ Protected method to remove all views (i.e. windows). """ for win in self.editors: self._removeView(win) def _removeView(self, win): """ Protected method to remove a view (i.e. window). @param win editor window to be removed """ self.__inRemoveView = True for tw in self.tabWidgets: if tw.hasEditor(win): tw.removeWidget(win) break win.closeIt() self.__inRemoveView = False # if this was the last editor in this view, switch to the next, that # still has open editors for i in list(range(self.tabWidgets.index(tw), -1, -1)) + \ list(range(self.tabWidgets.index(tw) + 1, len(self.tabWidgets))): if self.tabWidgets[i].hasEditors(): self.currentTabWidget.showIndicator(False) self.currentTabWidget = self.tabWidgets[i] self.currentTabWidget.showIndicator(True) self.activeWindow().setFocus() break aw = self.activeWindow() fn = aw and aw.getFileName() or None if fn: self.changeCaption.emit(fn) self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, aw.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(aw) def _addView(self, win, fn=None, noName="", next=False): """ Protected method to add a view (i.e. window). @param win editor assembly to be added @param fn filename of this editor (string) @param noName name to be used for an unnamed editor (string) @param next flag indicating to add the view next to the current view (bool) """ editor = win.getEditor() if fn is None: if not noName: self.untitledCount += 1 noName = self.tr("Untitled {0}").format(self.untitledCount) if next: index = self.currentTabWidget.currentIndex() + 1 self.currentTabWidget.insertWidget(index, win, noName) else: self.currentTabWidget.addTab(win, noName) editor.setNoName(noName) else: if self.filenameOnly: txt = os.path.basename(fn) else: txt = e5App().getObject("Project").getRelativePath(fn) if len(txt) > self.maxFileNameChars: txt = "...{0}".format(txt[-self.maxFileNameChars:]) if not QFileInfo(fn).isWritable(): txt = self.tr("{0} (ro)").format(txt) if next: index = self.currentTabWidget.currentIndex() + 1 self.currentTabWidget.insertWidget(index, win, txt) else: self.currentTabWidget.addTab(win, txt) index = self.currentTabWidget.indexOf(win) self.currentTabWidget.setTabToolTip(index, fn) self.currentTabWidget.setCurrentWidget(win) win.show() editor.setFocus() if fn: self.changeCaption.emit(fn) self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(editor) def insertView(self, win, tabWidget, index, fn=None, noName=""): """ Public method to add a view (i.e. window). @param win editor assembly to be inserted @param tabWidget reference to the tab widget to insert the editor into (TabWidget) @param index index position to insert at (integer) @param fn filename of this editor (string) @param noName name to be used for an unnamed editor (string) """ editor = win.getEditor() if fn is None: if not noName: self.untitledCount += 1 noName = self.tr("Untitled {0}").format(self.untitledCount) tabWidget.insertWidget(index, win, noName) editor.setNoName(noName) else: if self.filenameOnly: txt = os.path.basename(fn) else: txt = e5App().getObject("Project").getRelativePath(fn) if len(txt) > self.maxFileNameChars: txt = "...{0}".format(txt[-self.maxFileNameChars:]) if not QFileInfo(fn).isWritable(): txt = self.tr("{0} (ro)").format(txt) nindex = tabWidget.insertWidget(index, win, txt) tabWidget.setTabToolTip(nindex, fn) tabWidget.setCurrentWidget(win) win.show() editor.setFocus() if fn: self.changeCaption.emit(fn) self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(editor) self._modificationStatusChanged(editor.isModified(), editor) self._checkActions(editor) def _showView(self, win, fn=None): """ Protected method to show a view (i.e. window). @param win editor assembly to be shown @param fn filename of this editor (string) """ win.show() editor = win.getEditor() for tw in self.tabWidgets: if tw.hasEditor(editor): tw.setCurrentWidget(win) self.currentTabWidget.showIndicator(False) self.currentTabWidget = tw self.currentTabWidget.showIndicator(True) break editor.setFocus() def activeWindow(self): """ Public method to return the active (i.e. current) window. @return reference to the active editor """ cw = self.currentTabWidget.currentWidget() if cw: return cw.getEditor() else: return None def showWindowMenu(self, windowMenu): """ Public method to set up the viewmanager part of the Window menu. @param windowMenu reference to the window menu """ pass def _initWindowActions(self): """ Protected method to define the user interface actions for window handling. """ pass def setEditorName(self, editor, newName): """ Public method to change the displayed name of the editor. @param editor editor window to be changed @param newName new name to be shown (string) """ if newName: if self.filenameOnly: tabName = os.path.basename(newName) else: tabName = e5App().getObject("Project").getRelativePath(newName) if len(tabName) > self.maxFileNameChars: tabName = "...{0}".format(tabName[-self.maxFileNameChars:]) index = self.currentTabWidget.indexOf(editor) self.currentTabWidget.setTabText(index, tabName) self.currentTabWidget.setTabToolTip(index, newName) self.changeCaption.emit(newName) def _modificationStatusChanged(self, m, editor): """ Protected slot to handle the modificationStatusChanged signal. @param m flag indicating the modification status (boolean) @param editor editor window changed """ for tw in self.tabWidgets: if tw.hasEditor(editor): break index = tw.indexOf(editor) keys = [] if m: keys.append("fileModified.png") if editor.hasSyntaxErrors(): keys.append("syntaxError22.png") elif editor.hasWarnings(): keys.append("warning22.png") if not keys: keys.append("empty.png") tw.setTabIcon(index, UI.PixmapCache.getCombinedIcon(keys)) self._checkActions(editor) def _syntaxErrorToggled(self, editor): """ Protected slot to handle the syntaxerrorToggled signal. @param editor editor that sent the signal """ for tw in self.tabWidgets: if tw.hasEditor(editor): break index = tw.indexOf(editor) keys = [] if editor.isModified(): keys.append("fileModified.png") if editor.hasSyntaxErrors(): keys.append("syntaxError22.png") elif editor.hasWarnings(): keys.append("warning22.png") if not keys: keys.append("empty.png") tw.setTabIcon(index, UI.PixmapCache.getCombinedIcon(keys)) ViewManager._syntaxErrorToggled(self, editor) def addSplit(self): """ Public method used to split the current view. """ tw = TabWidget(self) tw.show() self.__splitter.addWidget(tw) self.tabWidgets.append(tw) self.currentTabWidget.showIndicator(False) self.currentTabWidget = self.tabWidgets[-1] self.currentTabWidget.showIndicator(True) tw.currentChanged.connect(self.__currentChanged) tw.installEventFilter(self) tw.tabBar().installEventFilter(self) if self.__splitter.orientation() == Qt.Horizontal: size = self.width() else: size = self.height() self.__splitter.setSizes([int(size / len(self.tabWidgets))] * len(self.tabWidgets)) self.splitRemoveAct.setEnabled(True) self.nextSplitAct.setEnabled(True) self.prevSplitAct.setEnabled(True) def removeSplit(self): """ Public method used to remove the current split view. @return flag indicating successfull removal """ if len(self.tabWidgets) > 1: tw = self.currentTabWidget res = True savedEditors = tw.editors[:] for editor in savedEditors: res &= self.closeEditor(editor) if res: try: i = self.tabWidgets.index(tw) except ValueError: return True if i == len(self.tabWidgets) - 1: i -= 1 self.tabWidgets.remove(tw) tw.close() self.currentTabWidget = self.tabWidgets[i] for tw in self.tabWidgets: tw.showIndicator(tw == self.currentTabWidget) if self.currentTabWidget is not None: assembly = self.currentTabWidget.currentWidget() if assembly is not None: editor = assembly.getEditor() if editor is not None: editor.setFocus(Qt.OtherFocusReason) if len(self.tabWidgets) == 1: self.splitRemoveAct.setEnabled(False) self.nextSplitAct.setEnabled(False) self.prevSplitAct.setEnabled(False) return True return False def getSplitOrientation(self): """ Public method to get the orientation of the split view. @return orientation of the split (Qt.Horizontal or Qt.Vertical) """ return self.__splitter.orientation() def setSplitOrientation(self, orientation): """ Public method used to set the orientation of the split view. @param orientation orientation of the split (Qt.Horizontal or Qt.Vertical) """ self.__splitter.setOrientation(orientation) def nextSplit(self): """ Public slot used to move to the next split. """ aw = self.activeWindow() _hasFocus = aw and aw.hasFocus() ind = self.tabWidgets.index(self.currentTabWidget) + 1 if ind == len(self.tabWidgets): ind = 0 self.currentTabWidget.showIndicator(False) self.currentTabWidget = self.tabWidgets[ind] self.currentTabWidget.showIndicator(True) if _hasFocus: aw = self.activeWindow() if aw: aw.setFocus() def prevSplit(self): """ Public slot used to move to the previous split. """ aw = self.activeWindow() _hasFocus = aw and aw.hasFocus() ind = self.tabWidgets.index(self.currentTabWidget) - 1 if ind == -1: ind = len(self.tabWidgets) - 1 self.currentTabWidget.showIndicator(False) self.currentTabWidget = self.tabWidgets[ind] self.currentTabWidget.showIndicator(True) if _hasFocus: aw = self.activeWindow() if aw: aw.setFocus() def __currentChanged(self, index): """ Private slot to handle the currentChanged signal. @param index index of the current tab (integer) """ if index == -1 or not self.editors: return editor = self.activeWindow() if editor is None: return self._checkActions(editor) editor.setFocus() fn = editor.getFileName() if fn: self.changeCaption.emit(fn) if not self.__inRemoveView: self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(editor) def eventFilter(self, watched, event): """ Public method called to filter the event queue. @param watched the QObject being watched (QObject) @param event the event that occurred (QEvent) @return always False """ if event.type() == QEvent.MouseButtonPress and \ not event.button() == Qt.RightButton: switched = True self.currentTabWidget.showIndicator(False) if isinstance(watched, E5TabWidget): switched = watched is not self.currentTabWidget self.currentTabWidget = watched elif isinstance(watched, QTabBar): switched = watched.parent() is not self.currentTabWidget self.currentTabWidget = watched.parent() if switched: index = self.currentTabWidget.selectTab(event.pos()) switched = self.currentTabWidget.widget(index) is \ self.activeWindow() elif isinstance(watched, QScintilla.Editor.Editor): for tw in self.tabWidgets: if tw.hasEditor(watched): switched = tw is not self.currentTabWidget self.currentTabWidget = tw break self.currentTabWidget.showIndicator(True) aw = self.activeWindow() if aw is not None: self._checkActions(aw) aw.setFocus() fn = aw.getFileName() if fn: self.changeCaption.emit(fn) if switched: self.editorChanged.emit(fn) self.editorLineChanged.emit( fn, aw.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(aw) return False def preferencesChanged(self): """ Public slot to handle the preferencesChanged signal. """ ViewManager.preferencesChanged(self) self.maxFileNameChars = Preferences.getUI( "TabViewManagerFilenameLength") self.filenameOnly = Preferences.getUI("TabViewManagerFilenameOnly") for tabWidget in self.tabWidgets: for index in range(tabWidget.count()): editor = tabWidget.widget(index) if isinstance(editor, QScintilla.Editor.Editor): fn = editor.getFileName() if fn: if self.filenameOnly: txt = os.path.basename(fn) else: txt = e5App().getObject("Project")\ .getRelativePath(fn) if len(txt) > self.maxFileNameChars: txt = "...{0}".format(txt[-self.maxFileNameChars:]) if not QFileInfo(fn).isWritable(): txt = self.tr("{0} (ro)").format(txt) tabWidget.setTabText(index, txt) def getTabWidgetById(self, id_): """ Public method to get a reference to a tab widget knowing its ID. @param id_ id of the tab widget (long) @return reference to the tab widget (TabWidget) """ for tw in self.tabWidgets: if id(tw) == id_: return tw return None
class SignalTabController(QWidget): frame_closed = pyqtSignal(SignalFrameController) not_show_again_changed = pyqtSignal() signal_created = pyqtSignal(Signal) files_dropped = pyqtSignal(list) frame_was_dropped = pyqtSignal(int, int) @property def num_frames(self): return self.splitter.count() - 1 @property def signal_frames(self): """ :rtype: list of SignalFrameController """ return [self.splitter.widget(i) for i in range(self.num_frames)] @property def signal_undo_stack(self): return self.undo_stack def __init__(self, project_manager, parent=None): super().__init__(parent) self.ui = Ui_Interpretation() self.ui.setupUi(self) self.splitter = QSplitter() self.splitter.setStyleSheet("QSplitter::handle:vertical {\nmargin: 4px 0px; background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, \nstop:0 rgba(255, 255, 255, 0), \nstop:0.5 rgba(100, 100, 100, 100), \nstop:1 rgba(255, 255, 255, 0));\n image: url(:/icons/data/icons/splitter_handle_horizontal.svg);\n}") self.splitter.setOrientation(Qt.Vertical) self.splitter.setChildrenCollapsible(True) self.splitter.setHandleWidth(6) placeholder_widget = QWidget() placeholder_widget.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.undo_stack = QUndoStack() self.project_manager = project_manager self.splitter.addWidget(placeholder_widget) self.signal_vlay = QVBoxLayout() self.signal_vlay.setContentsMargins(0,0,0,0) self.signal_vlay.addWidget(self.splitter) self.ui.scrlAreaSignals.setLayout(self.signal_vlay) self.drag_pos = None def on_files_dropped(self, files): self.files_dropped.emit(files) def close_frame(self, frame:SignalFrameController): self.frame_closed.emit(frame) def add_signal_frame(self, proto_analyzer): sig_frame = SignalFrameController(proto_analyzer, self.undo_stack, self.project_manager, parent=self) sframes = self.signal_frames if len(proto_analyzer.signal.filename) == 0: # new signal from "create signal from selection" sig_frame.ui.btnSaveSignal.show() self.__create_connects_for_signal_frame(signal_frame=sig_frame) sig_frame.signal_created.connect(self.signal_created.emit) sig_frame.not_show_again_changed.connect(self.not_show_again_changed.emit) sig_frame.ui.lineEditSignalName.setToolTip(self.tr("Sourcefile: ") + proto_analyzer.signal.filename) sig_frame.apply_to_all_clicked.connect(self.on_apply_to_all_clicked) prev_signal_frame = sframes[-1] if len(sframes) > 0 else None if prev_signal_frame is not None and hasattr(prev_signal_frame, "ui"): sig_frame.ui.cbProtoView.setCurrentIndex(prev_signal_frame.ui.cbProtoView.currentIndex()) sig_frame.blockSignals(True) if proto_analyzer.signal.qad_demod_file_loaded: sig_frame.ui.cbSignalView.setCurrentIndex(1) sig_frame.ui.cbSignalView.setDisabled(True) self.splitter.insertWidget(self.num_frames, sig_frame) sig_frame.blockSignals(False) default_view = constants.SETTINGS.value('default_view', 0, int) sig_frame.ui.cbProtoView.setCurrentIndex(default_view) return sig_frame def __create_connects_for_signal_frame(self, signal_frame: SignalFrameController): signal_frame.hold_shift = constants.SETTINGS.value('hold_shift_to_drag', False, type=bool) signal_frame.drag_started.connect(self.frame_dragged) signal_frame.frame_dropped.connect(self.frame_dropped) signal_frame.files_dropped.connect(self.on_files_dropped) signal_frame.closed.connect(self.close_frame) def add_empty_frame(self, filename: str, proto): sig_frame = SignalFrameController(proto_analyzer=proto, undo_stack=self.undo_stack, project_manager=self.project_manager, proto_bits=proto.decoded_proto_bits_str, parent=self) sig_frame.ui.lineEditSignalName.setText(filename) sig_frame.setMinimumHeight(sig_frame.height()) sig_frame.set_empty_frame_visibilities() self.__create_connects_for_signal_frame(signal_frame=sig_frame) self.splitter.insertWidget(self.num_frames, sig_frame) return sig_frame def set_frame_numbers(self): for i, f in enumerate(self.signal_frames): f.ui.lSignalNr.setText("{0:d}:".format(i + 1)) @pyqtSlot() def save_all(self): if self.num_frames == 0: return settings = constants.SETTINGS try: not_show = settings.value('not_show_save_dialog', type=bool, defaultValue=False) except TypeError: not_show = False if not not_show: cb = QCheckBox("Don't ask me again.") msg_box = QMessageBox(QMessageBox.Question, self.tr("Confirm saving all signals"), self.tr("All changed signal files will be overwritten. OK?")) msg_box.addButton(QMessageBox.Yes) msg_box.addButton(QMessageBox.No) msg_box.setCheckBox(cb) reply = msg_box.exec() not_show_again = cb.isChecked() settings.setValue("not_show_save_dialog", not_show_again) self.not_show_again_changed.emit() if reply != QMessageBox.Yes: return for f in self.signal_frames: if f.signal is None or f.signal.filename == "": continue f.signal.save() @pyqtSlot() def close_all(self): for f in self.signal_frames: f.my_close() @pyqtSlot(Signal) def on_apply_to_all_clicked(self, signal: Signal): for frame in self.signal_frames: if frame.signal is not None: frame.signal.noise_min_plot = signal.noise_min_plot frame.signal.noise_max_plot = signal.noise_max_plot frame.signal.block_protocol_update = True proto_needs_update = False if frame.signal.modulation_type != signal.modulation_type: frame.signal.modulation_type = signal.modulation_type proto_needs_update = True if frame.signal.qad_center != signal.qad_center: frame.signal.qad_center = signal.qad_center proto_needs_update = True if frame.signal.tolerance != signal.tolerance: frame.signal.tolerance = signal.tolerance proto_needs_update = True if frame.signal.noise_threshold != signal.noise_threshold: frame.signal.noise_threshold = signal.noise_threshold proto_needs_update = True if frame.signal.bit_len != signal.bit_len: frame.signal.bit_len = signal.bit_len proto_needs_update = True frame.signal.block_protocol_update = False if proto_needs_update: frame.signal.protocol_needs_update.emit() @pyqtSlot(QPoint) def frame_dragged(self, pos: QPoint): self.drag_pos = pos @pyqtSlot(QPoint) def frame_dropped(self, pos: QPoint): start = self.drag_pos if start is None: return end = pos start_index = -1 end_index = -1 if self.num_frames > 1: for i, w in enumerate(self.signal_frames): if w.geometry().contains(start): start_index = i if w.geometry().contains(end): end_index = i self.swap_frames(start_index, end_index) self.frame_was_dropped.emit(start_index, end_index) @pyqtSlot(int, int) def swap_frames(self, from_index: int, to_index: int): if from_index != to_index: start_sig_widget = self.splitter.widget(from_index) self.splitter.insertWidget(to_index, start_sig_widget) @pyqtSlot() def on_participant_changed(self): for sframe in self.signal_frames: sframe.on_participant_changed()
class EntryView(BaseTransactionView): def _setup(self): self._setupUi() self.etable = EntryTable(self.model.etable, view=self.tableView) self.efbar = EntryFilterBar(model=self.model.filter_bar, view=self.filterBar) self.bgraph = Chart(self.model.bargraph, view=self.barGraphView) self.lgraph = Chart(self.model.balgraph, view=self.lineGraphView) self._setupColumns() # Can only be done after the model has been connected self.reconciliationButton.clicked.connect(self.model.toggle_reconciliation_mode) def _setupUi(self): self.resize(483, 423) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setSpacing(0) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout = QHBoxLayout() self.horizontalLayout.setSpacing(0) self.filterBar = RadioBox(self) sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.filterBar.sizePolicy().hasHeightForWidth()) self.filterBar.setSizePolicy(sizePolicy) self.horizontalLayout.addWidget(self.filterBar) self.horizontalLayout.addItem(horizontalSpacer()) self.reconciliationButton = QPushButton(tr("Reconciliation")) self.reconciliationButton.setCheckable(True) self.horizontalLayout.addWidget(self.reconciliationButton) self.verticalLayout.addLayout(self.horizontalLayout) self.splitterView = QSplitter() self.splitterView.setOrientation(Qt.Vertical) self.splitterView.setChildrenCollapsible(False) self.tableView = TableView(self) self.tableView.setAcceptDrops(True) self.tableView.setEditTriggers(QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed) self.tableView.setDragEnabled(True) self.tableView.setDragDropMode(QAbstractItemView.InternalMove) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setSortingEnabled(True) self.tableView.horizontalHeader().setHighlightSections(False) self.tableView.horizontalHeader().setMinimumSectionSize(18) self.tableView.verticalHeader().setVisible(False) self.tableView.verticalHeader().setDefaultSectionSize(18) self.splitterView.addWidget(self.tableView) self.graphView = QStackedWidget(self) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.graphView.sizePolicy().hasHeightForWidth()) self.graphView.setSizePolicy(sizePolicy) self.graphView.setMinimumSize(0, 200) self.lineGraphView = LineGraphView() self.graphView.addWidget(self.lineGraphView) self.barGraphView = BarGraphView() self.graphView.addWidget(self.barGraphView) self.splitterView.addWidget(self.graphView) self.graphView.setCurrentIndex(1) self.splitterView.setStretchFactor(0, 1) self.splitterView.setStretchFactor(1, 0) self.verticalLayout.addWidget(self.splitterView) def _setupColumns(self): h = self.tableView.horizontalHeader() h.setSectionsMovable(True) # column drag & drop reorder # --- QWidget override def setFocus(self): self.etable.view.setFocus() # --- Public def fitViewsForPrint(self, viewPrinter): hidden = self.model.mainwindow.hidden_areas viewPrinter.fitTable(self.etable) if PaneArea.BottomGraph not in hidden: viewPrinter.fit(self.graphView.currentWidget(), 300, 150, expandH=True, expandV=True) def restoreSubviewsSize(self): graphHeight = self.model.graph_height_to_restore if graphHeight: splitterHeight = self.splitterView.height() sizes = [splitterHeight-graphHeight, graphHeight] self.splitterView.setSizes(sizes) # --- model --> view def refresh_reconciliation_button(self): if self.model.can_toggle_reconciliation_mode: self.reconciliationButton.setEnabled(True) self.reconciliationButton.setChecked(self.model.reconciliation_mode) else: self.reconciliationButton.setEnabled(False) self.reconciliationButton.setChecked(False) def show_bar_graph(self): self.graphView.setCurrentIndex(1) def show_line_graph(self): self.graphView.setCurrentIndex(0) def update_visibility(self): hidden = self.model.mainwindow.hidden_areas self.graphView.setHidden(PaneArea.BottomGraph in hidden)
class MainWindow(QMainWindow): def __init__(self, settings): super().__init__() # Save the settings object for now and shutdown time. self.settings = settings # Initialize extras and dicts paths first, as other modules use them paths.initialize(settings) # Initialize our font db fonts.initialize(settings) # Set our font, which will propogate to our child widgets. fonts.notify_me(self._font_change) # ask for a signal self._font_change(False) # fake a signal now # Initialize the dictionary apparatus dictionaries.initialize(settings) # Initialize the color choices colors.initialize(settings) # Initialize the sequence number for opened files self.book_number = 0 # Initialize the path to the last-opened file, used to # start file-open dialogs. self.last_open_path = '.' # Initialize our dict of active panels self.panel_dict = PANEL_DICT.copy() # Initialize our dict of open documents {seqno:Book} self.open_books = {} self.focus_book = None # seqno of book in focus, see _focus_me # Initialize the list of recent files self.recent_files = [] # Initialize the handle of a help display widget self.help_widget = None # later, if at all # Finished initializing after the app is running QTimer.singleShot(300, self.finish_init) # Create the main window and set up the menus. self._uic() # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # As part of setup we often need to show the user a dialog, but when our # __init__ is first called, our window has not been shown and the # app.exec_() call has not been made. If ok_cancel_msg() is used in that # condition, there is a big delay and spinning cursor on the mac. So this # code is called from a one-shot timer 300ms after the window has been # created, so we are sure the app is processing events etc. def finish_init(self): #self.finish_init = False # never do this again # Initialize the set of files actually open when we shut down. last_session = self._read_flist('mainwindow/open_files') if len(last_session): # there were some files open if len(last_session) == 1: msg = _TR('Start-up dialog', 'One book was open at the end of the last session.') else: msg = _TR('Start-up dialog', '%n books were open at the end of the last session.', n=len(last_session)) info = _TR("Start-up dialog", "Click OK to re-open all") if utilities.ok_cancel_msg(msg, info, parent=self): for file_path in last_session: ftbs = utilities.path_to_stream(file_path) if ftbs: self._open(ftbs) if 0 == len(self.open_books): # We did not re-open any books, either because there were # none, or the user said No, or perhaps they were not found. self._new() # open one, new, book. # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Slot to receive the currentChanged signal from the editview tabset. # Look through self.open_books and find the one whose edit widget is # now current, and do a focus_me for it. def _editview_change(self, index): if index > -1: eview = self.editview_tabset.widget(index) for (seqno, book) in self.open_books.items(): if eview == book.get_edit_view(): self.focus_me(seqno) return mainwindow_logger.error('cannot relate editview tab index to book') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Make a selected book the focus of all panels. This is called explicitly # when a book is first created, and when the editview tabset changes the # current selection. It is called also when an editview gets a focus-in # event. # Display that Book's various "-view" objects in panels, in the order # that the user left them and with the same active panel as before. Note # that a book (editview) can get a focus-in event when it was already the # focus in this sense, for example if this app was hidden and then # brought to the front. So be prepared for redundant calls. def focus_me(self, book_index): outgoing = self.focus_book if book_index == outgoing: return # redundant call mainwindow_logger.debug('focusing {0} = {1}'.format( book_index, self.open_books[book_index].get_book_name())) self.focus_book = book_index # Record the user's arrangement of panels for the outgoing book, # as a list of tuples ('tabname', widget) in correct sequence. if outgoing is not None: # false first time and after File>Close out_panel_dict = self.open_books[outgoing].panel_dict widg_list = [] for ix in range(self.panel_tabset.count()): widg_list.append((self.panel_tabset.tabText(ix), self.panel_tabset.widget(ix))) out_panel_dict['tab_list'] = widg_list out_panel_dict['current'] = self.panel_tabset.currentIndex() # Change all the panels to the widgets, in the sequence, of the new book in_panel_dict = self.open_books[book_index].panel_dict widg_list = in_panel_dict['tab_list'] self.panel_tabset.clear() for ix in range(len(widg_list)): (tab_text, widget) = widg_list[ix] self.panel_tabset.insertTab(ix, widget, tab_text) self.panel_tabset.setCurrentIndex(in_panel_dict['current']) self.editview_tabset.setCurrentIndex( self.editview_tabset.indexOf( self.open_books[book_index].get_edit_view())) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Called by the current book to make a particular tab the visible one, # e.g. to make the Find visible on a ^F. The argument is a widget # that should occupy one of the current tabs. Ask the tabset for its # index, and if it is found, make that the current index. (If it is # not found, log it and do nothing.) def make_tab_visible(self, tabwidg): ix = self.panel_tabset.indexOf(tabwidg) if ix >= 0: # widget exists in this tabset self.panel_tabset.setCurrentIndex(ix) return mainwindow_logger.error('Request to show nonexistent widget') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement File>New: # Create a Book object # Call its new_empty() method, # Add it to the open_books dict keyed by its sequence number, # Display its text editor in a tab with the document name, and # Give it the focus. def _new(self): seq = self.book_number self.book_number += 1 new_book = book.Book(seq, self) new_book.new_empty() self.open_books[seq] = new_book index = self.editview_tabset.addTab(new_book.get_edit_view(), new_book.get_book_name()) self.editview_tabset.setTabToolTip( index, _TR('Tooltip of edit of new unsaved file', 'this file has not been saved')) self.focus_me(seq) # # For use from translators, do the New operation and return the Book # that is created so it can be loaded with translated text. def do_new(self): self._new() return self.open_books[self.focus_book] # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Quick check to see if a file path is already open. Called from _open # and from _build_recent (menu). Returned value is the sequence number # of the open book, or None. def _is_already_open(self, path): for (seq, book_object) in self.open_books.items(): if path == book_object.get_book_full_path(): return seq return None # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement File>Open. Dialog with the user (file dialog starts with # last-used book path). Result is None or a FileBasedTextStream that we # pass to _open(). def _file_open(self): fbts = utilities.ask_existing_file(_TR('File:Open dialog', 'Select a book file to open'), parent=self, starting_path=self.last_open_path) if fbts: # yes a readable file was chosen. self._open(fbts) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Open a file, given the document as a FileBasedTextStream, and META # means our metafile suffix (C.METAFILE_SUFFIX). # * If file opened is fname.META, look for a file named fname; if it # exists open it instead, e.g. given foo.txt.META, open foo.txt. # If it doesn't exist, tell the user and exit. # * If a file of the same name and path is already open, just focus # it and exit. # * Determine if there is a .META file, a .bin file, or neither # * Create a metadata input stream if possible # * If no .META, look for good_words and bad_words # * If the only open book is an "Untitled-n" and # it is unmodified, delete it. # * Call Book.old_book() or .new_book() as appropriate # * Add this book's editview to the edit tabset # * Give this book the focus. def _open(self, fbts): # look for opening a .META file if C.METAFILE_SUFFIX == fbts.suffix(): fb2 = utilities.file_less_suffix(fbts) if fb2 is None: m1 = _TR('File:Open', 'Cannot open a metadata file alone') m2 = _TR('File:Open', 'There is no book file matching ', 'filename follows this') + fbts.filename() utilities.warning_msg(m1, m2, parent=self) return # we see foo.txt with foo.txt.META, silently open it fbts = fb2 # look for already-open file seq = self._is_already_open(fbts.fullpath()) if seq is not None: self.focus_me(seq) return # start collecting auxiliary streams gw_stream = None bw_stream = None #gg_stream = None # open the metadata stream, which is always UTF-8 meta_stream = utilities.related_suffix(fbts, C.METAFILE_SUFFIX, encoding=C.ENCODING_UTF) if meta_stream is None: # opening book without metadata; look for .bin which is always LTN1 # This is no longer supported - somebody who cares, can write a # .bin-to-JSON utility if they want. #bin_stream = utilities.related_suffix(fbts,'bin',encoding=C.ENCODING_LATIN) #if bin_stream : #gg_stream = metadata.translate_bin(bin_stream,fbts) # Look for good_words.txt, bad_words.txt. gw_stream = utilities.related_file(fbts, 'good_words*.*') bw_stream = utilities.related_file(fbts, 'bad_words*.*') seq = self.book_number # If the only open book is the new one created at startup or when all # books are closed (which will have key 0), and it has not been # modified, get rid of it. if len(self.open_books) == 1 \ and 0 == list(self.open_books.keys())[0] \ and self.open_books[0].get_book_name().startswith('Untitled-') \ and not self.open_books[0].get_save_needed() : self.editview_tabset.clear() self.panel_tabset.clear() self.focus_book = None seq = 0 else: # Some other book open, or user typed into the default New one. self.book_number += 1 # Make the Book object and stow it in our open book dict a_book = book.Book(seq, self) self.open_books[seq] = a_book if meta_stream: # opening a book we previously saved a_book.old_book(fbts, meta_stream) else: a_book.new_book(fbts, gw_stream, bw_stream) index = self.editview_tabset.addTab(a_book.get_edit_view(), a_book.get_book_name()) self.editview_tabset.setTabToolTip(index, a_book.get_book_folder()) self.focus_me(seq) self.last_open_path = fbts.folderpath() # start for next open or save self._add_to_recent(fbts.fullpath()) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Save the book that is currently in focus under its present name, if it # is modified. Return True if the save completed, else False. # If the active book is a New one, force a Save-As action instead. def _save(self): active_book = self.open_books[self.focus_book] if active_book.get_save_needed(): if active_book.get_book_name().startswith('Untitled-'): return self._save_as() doc_stream = utilities.path_to_output( active_book.get_book_full_path()) if doc_stream: # successfully opened for output meta_stream = utilities.related_output(doc_stream, C.METAFILE_SUFFIX) if not meta_stream: utilities.warning_msg( _TR('File:Save', 'Unable to open metadata file for writing.'), _TR('File:Save', 'Use loglevel=error for details.'), parent=self) return False else: utilities.warning_msg(_TR( 'File:Save', 'Unable to open book file for writing.'), _TR('File:Save', 'Use loglevel=error for details.'), parent=self) return False return active_book.save_book(doc_stream, meta_stream) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement Save As. Query the user for a file path and get that as an # output FileBasedTextStream. Call the book to rename itself, which makes # it modified. Change the text in the edit tab to match. Discard the FBTS # and call _save which will make another one. def _save_as(self): active_book = self.open_books[self.focus_book] fbts = utilities.ask_saving_file( _TR('File:Save As dialog', 'Choose a new location and filename for this book'), self, active_book.get_book_folder()) if fbts: active_book.rename_book(fbts) self.editview_tabset.setTabText( self.editview_tabset.currentIndex(), fbts.filename()) self.editview_tabset.setTabToolTip( self.editview_tabset.currentIndex(), active_book.get_book_folder()) self._add_to_recent(fbts.fullpath()) fbts = None # discard that object return self._save() else: return False # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement Close. If the active book is modified, ask if it should # be saved. If it is 'Untitled-' that will turn into Save As. def _close(self): target_index = self.focus_book # active edit tab is to close target_book = self.open_books[target_index] if target_book.get_save_needed(): # Compose message of translated parts because _TR does not # allow for incorporating strings, only numbers. msg = _TR('File Close dialog', 'Book file ', 'filename follows here') msg += target_book.get_book_name() msg += _TR('File Close dialog', ' has been modified!', 'filename precedes this') ret = utilities.save_discard_cancel_msg( msg, info=_TR('File Close dialog', 'Save it, Discard changes, or Cancel Closing?'), parent=self) if ret is None: # Cancel return if ret: # True==Save self._save() # Now, get rid of the active book in 3 steps, # 1, close the book's tab in the editview tabset. We don't know which # tab it is, because the user can drag tabs around. i = self.editview_tabset.indexOf(target_book.get_edit_view()) # The following causes another tab to be focussed, changing self.focus_book # and saving target_book's tabs in target_book, not that we care. self.editview_tabset.removeTab(i) # 2, remove the book from our dict of open books. del self.open_books[target_index] # 3, if there are any open books remaining, the tab widget has # activated one of them by its rules, which caused a show signal and # entry to _focus_me already. However if there are no remaining books # there was no show signal or focus_me and the closed book's panels # are still in the tabset. if 0 == len(self.open_books): self.book_number = 0 # restart the sequence self.focus_book = None self._new() # One way or the other, a focus_me has removed all references to # active_book's view panels except those in its PANEL_DICT. So the # following assignment should remove the last reference to the book, # and schedule the book and associated objects for garbage collect. target_book = None # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement a Translate... submenu command. Call translators.xlt_book # with the book object that is currently in focus. If the process works # xlt_book will create a new book by calling do_new() and load it. # def _xlt_a_book(self): book = self.open_books[self.focus_book] datum = self.sender().data() translators.xlt_book(book, datum, self) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement loading and saving find panel user buttons. Start the search # for files in the active book's folder. User can navigate to extras # if need be. def _find_save(self): target_book = self.open_books[self.focus_book] find_panel = target_book.get_find_panel() stream = utilities.ask_saving_file( _TR('File:Save Find Buttons open dialog', 'Choose file to contain find button definitions'), self, starting_path=target_book.get_last_find_button_path(), encoding='UTF-8') if stream: # is not None, file is open target_book.set_last_find_button_path(stream.fullpath()) find_panel.user_button_output(stream) # else user hit cancel, forget it def _find_load(self): target_book = self.open_books[self.focus_book] find_panel = target_book.get_find_panel() stream = utilities.ask_existing_file( _TR('File:Load Find Buttons open dialog', 'Choose a file of find button definitions'), self, starting_path=target_book.get_last_find_button_path(), encoding='UTF-8') if stream: # is not None, we opened it target_book.set_last_find_button_path(stream.fullpath()) find_panel.user_button_input(stream) target_book.metadata_modified(True, C.MD_MOD_FLAG) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Maintain the list of "recent" file paths. The list is kept in usage # order, so if a path is in the list now, delete it and then add it to # the front. Keep it at a max of 9 items by deleting the oldest if # necessary. def _add_to_recent(self, path): if path in self.recent_files: del self.recent_files[self.recent_files.index(path)] self.recent_files.insert(0, path) self.recent_files = self.recent_files[:9] # Upon the aboutToShow signal from the File menu, populate the Recent # submenu with a list of files, but only the ones that are currently # accessible. If one is on a volume (e.g. USB stick) and you unmount the # volume, the path should not appear in the menu until the volume is # mounted again. def _open_recent(self): path = self.sender().data() fbts = utilities.path_to_stream(path) if fbts: self._open(fbts) def _build_recent(self): active_files = [] self.recent_menu.clear() self.recent_menu.setEnabled(False) for path in self.recent_files: seq = self._is_already_open(path) if (seq is None) and utilities.file_is_accessible(path): active_files.append(path) if 0 == len(active_files): return self.recent_menu.setEnabled(True) for (i, path) in enumerate(active_files, start=1): (folder, fname) = os.path.split(path) act = self.recent_menu.addAction('{0} {1} {2}'.format( i, fname, folder)) act.setData(path) act.triggered.connect(self._open_recent) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # User has chosen a different font; if it is the general font, set # that here so it will propogate to our children. n.b. this is never # used as the preference for UI font is not implemented. def _font_change(self, is_mono): if not is_mono: self.setFont(fonts.get_general()) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Preferences menu action triggered. Create a Preferences dialog and # show it. def _preferences(self): p = preferences.PreferenceDialog(self) r = p.exec_() # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Help menu action triggered. If the Help widget has not yet been # created, create it. Otherwise just show it and raise it. def _show_help(self): if self.help_widget is None: self.help_widget = helpview.HelpWidget() self.help_widget.show() self.help_widget.raise_() # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Create the UI contained within this QMainWindow object. This is a lean # main window indeed. We have no toolbar, no status bar, no dock, # nothing. Just a splitter with, on the left, a tabset for editviews, and # on the right, a scrollbar containing a tabset for panels. (Qt Designer # note: it is not possible to build this structure with the Designer. It # will not let you put the scroll area into the splitter.) # # TODO: create a custom QTabWidget using a custom QTabBar to implement # drag-out-of-tabset behavior, and use those here. def _uic(self): global _EDIT_MENU # Create the tabset that displays editviews self.editview_tabset = QTabWidget() self.editview_tabset.setMovable(True) # let user move tabs around self.editview_tabset.currentChanged.connect(self._editview_change) # Create the tabset that displays find, notes, help &etc. self.panel_tabset = QTabWidget() self.panel_tabset.setMovable(True) # Create the splitter that contains the above two parts. self.splitter = QSplitter(Qt.Horizontal, self) self.splitter.setChildrenCollapsible(False) # Give just a little margin to the left of the editor self.splitter.setContentsMargins(8, 0, 0, 0) self.splitter.addWidget(self.editview_tabset) self.splitter.addWidget(self.panel_tabset) # Set that splitter as the main window's central (and only) widget self.setCentralWidget(self.splitter) # Populate the panel tabset with empty widgets just so there will # be tabs that _swap can reference. for key in self.panel_dict.keys(): widj = QWidget() self.panel_tabset.addTab(widj, key) self.panel_dict[key] = widj # Size and position ourself based on saved settings. self.move(self.settings.value("mainwindow/position", QPoint(50, 50))) self.resize( self.settings.value("mainwindow/size", C.STARTUP_DEFAULT_SIZE)) self.splitter.restoreState( self.settings.value("mainwindow/splitter", C.STARTUP_DEFAULT_SPLITTER)) self.restoreState( self.settings.value("mainwindow/windowstate", QByteArray())) # Store a reference to the application menubar. In Mac OS this # is a parentless menubar; other platforms it is the default. if C.PLATFORM_IS_MAC: self.menu_bar = QMenuBar() # parentless menu bar for Mac OS else: self.menu_bar = self.menuBar() # refer to the default one # Create the File menu, located in our menu_bar. self.file_menu = self.menu_bar.addMenu(_TR('Menu name', '&File')) # Populate the File menu with actions. # File:New -> _new() work = self.file_menu.addAction(_TR('File menu command', '&New')) work.setShortcut(QKeySequence.New) work.setToolTip(_TR('File:New tooltip', 'Create a new, empty document')) work.triggered.connect(self._new) # File:Open -> _file_open() work = self.file_menu.addAction(_TR('File menu command', '&Open')) work.setShortcut(QKeySequence.Open) work.setToolTip(_TR('File:Open tooltip', 'Open an existing book')) work.triggered.connect(self._file_open) # File:Save -> _file_save() work = self.file_menu.addAction(_TR('File menu command', '&Save')) work.setShortcut(QKeySequence.Save) work.setToolTip(_TR('File:Save tooltip', 'Save the active book')) work.triggered.connect(self._save) # Save As -> _file_save_as() work = self.file_menu.addAction(_TR('File menu command', 'Save &As')) work.setShortcut(QKeySequence.SaveAs) work.setToolTip( _TR('File:Save As tooltip', 'Save the active book under a new name')) work.triggered.connect(self._save_as) # Close -> _close() work = self.file_menu.addAction(_TR('File menu command', 'Close')) work.setShortcut(QKeySequence.Close) work.setToolTip(_TR('File:Close tooltip', 'Close the active book')) work.triggered.connect(self._close) # Load Find Buttons -> _find_load() work = self.file_menu.addAction( _TR('File menu command', 'Load Find Buttons')) work.setToolTip( _TR( 'File:Load Find Buttons tooltip', 'Load a file of definitions for the custom buttons in the Find panel' )) work.triggered.connect(self._find_load) # Save Find Buttons -> _find_save() work = self.file_menu.addAction( _TR('File menu command', 'Save Find Buttons')) work.setToolTip( _TR('File:Save Find Buttons tooltip', 'Save definitions of the custom buttons in the Find panel')) work.triggered.connect(self._find_save) # Translate... gets a submenu with an entry for every Translator # in extras/Translators (if any). The actions connect to _xlt_a_book. self.translate_submenu = translators.build_xlt_menu( self, self._xlt_a_book) self.file_menu.addMenu(self.translate_submenu) # Open Recent gets a submenu that is added to the File menu. # The aboutToShow signal is connected to our _build_recent slot. self.recent_menu = QMenu(_TR('Sub-menu name', '&Recent Files')) work = self.file_menu.addMenu(self.recent_menu) work.setToolTip( _TR('File:Recent tooltip', 'List of recently-used files to open')) self.file_menu.aboutToShow.connect(self._build_recent) # Put in a divider above the Help, Preferences and Quit actions. self.file_menu.addSeparator() # Help opens or un-hides the Help viewer work = self.file_menu.addAction(_TR('Help menu item', 'Help')) work.setToolTip( _TR('Help menu item tooltip', 'Display the Help/User Manual in a separate window')) work.triggered.connect(self._show_help) self.file_menu.addAction(work) # Preferences: On the Mac, Preferences is automatically moved to the app menu. work = self.file_menu.addAction( _TR('Preferences menu item', 'Preferences')) work.setToolTip( _TR( 'Preferences menu item tooltip', 'Open the Preferences dialog to set paths, fonts, and text styles' )) work.setMenuRole(QAction.PreferencesRole) work.triggered.connect(self._preferences) # Quit choice, with the menu role that moves it to the app menu work = QAction(_TR('Quit command', '&Quit'), self) work.setMenuRole(QAction.QuitRole) work.setShortcut(QKeySequence.Quit) work.triggered.connect(self.close) self.file_menu.addAction(work) # Initialize the list of "recent" files for the File sub-menu. # These files were not necessarily open at shutdown, just sometime # in the not too distant past. self.recent_files = self._read_flist('mainwindow/recent_files') # Create the Edit menu in the menu_bar, store a reference to it # in a static global, and immediately clear it. _EDIT_MENU = self.menu_bar.addMenu(C.ED_MENU_EDIT) hide_edit_menu() # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Functions related to shutdown and management of settings. # # Factor out the job of reading/writing a list of files in the settings. # Input is a settings array key string like 'mainwindow/recent_files' # Output is a possibly empty list of canonical-file-path strings. def _read_flist(self, array_key): f_list = [] f_count = self.settings.beginReadArray(array_key) for f in range(f_count): # which may be 0 self.settings.setArrayIndex(f) f_list.append(self.settings.value('filepath')) self.settings.endArray() return f_list # Input is an array key and a possibly empty list of path strings def _write_flist(self, file_list, array_key): if len(file_list): self.settings.beginWriteArray(array_key, len(file_list)) for f in range(len(file_list)): self.settings.setArrayIndex(f) self.settings.setValue('filepath', file_list[f]) self.settings.endArray() # Reimplement QWidget.closeEvent in order to save any open files # and update the settings. def closeEvent(self, event): # If there are any unsaved books, ask the user if they should be # saved. If the answer is yes, try to do so. unsaved = [] for (seq, book_object) in self.open_books.items(): if book_object.get_save_needed(): unsaved.append(seq) if len(unsaved): if len(unsaved) == 1: msg = _TR('Shutdown message', 'There is one unsaved file') else: msg = _TR('Shutdown message', 'There are %n unsaved files', n=len(unsaved)) ret = utilities.save_discard_cancel_msg( msg, _TR('Shutdown message', 'Save, Discard changes, or Cancel Quit?'), parent=self) if ret is None: # user wants to cancel shutdown event.ignore() return if ret: # User want to save. Focus each unsaved file and call _save. # For all but "Untitled-n" documents this will be silent. For # those, it will open a save-as dialog. We ignore the return # from this because we cannot distinguish between a cancelled # file-open dialog and a file write error. for seq in unsaved: self.focus_me(seq) self._save() # Clear the settings so that old values don't hang around self.settings.clear() # Tell the submodules to save their current global values. colors.shutdown(self.settings) fonts.shutdown(self.settings) dictionaries.shutdown(self.settings) paths.shutdown(self.settings) # Save the list of currently-open files in the settings, but do not # save any whose filename matches "Untitled-#" because that is an # unsaved New file (which the user chose not to save, above). open_paths = [] for (index, book_obj) in self.open_books.items(): if not book_obj.get_book_name().startswith('Untitled-'): open_paths.append(book_obj.get_book_full_path()) self._write_flist(open_paths, 'mainwindow/open_files') # Save the list of "recent" files in the settings. self._write_flist(self.recent_files, 'mainwindow/recent_files') # Save this window's position and size and splitter state self.settings.setValue("mainwindow/size", self.size()) self.settings.setValue("mainwindow/position", self.pos()) self.settings.setValue("mainwindow/splitter", self.splitter.saveState()) self.settings.setValue("mainwindow/windowstate", self.saveState()) # If the Help window is open, close it -- as a modeless widget, # it won't close just because we do. if self.help_widget: # is not None, self.help_widget.close() # and that's it, we are done finished, over & out. event.accept()
class ChartWidget(QWidget): def __init__(self, *args, **kwargs): super(ChartWidget, self).__init__(*args, **kwargs) self.resize(800, 600) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) self.splitter = QSplitter(self) layout.addWidget(self.splitter) self.splitter.setOrientation(Qt.Horizontal) self.splitter.setStretchFactor(0, 2) self.splitter.setStretchFactor(1, 3) self.splitter.setStretchFactor(2, 5) self.splitter.setAutoFillBackground(True) self.splitter.setChildrenCollapsible(False) self.splitter.setHandleWidth(2) # 分类 self.classifyWidget = ClassifyWidget(self) self.splitter.addWidget(self.classifyWidget) # 代码 self.codeWidget = CodeWidget(self) self.splitter.addWidget(self.codeWidget) # 等待界面 self.loadingWidget = LoadingWidget(self, visible=False) self.loadingWidget.resize(self.size()) # 绑定信号槽 self.classifyWidget.fileSelected.connect(self.codeWidget.openFile) self.codeWidget.runSignal.connect(self.onRun) def onRun(self, text): self.setEnabled(False) self.loadingWidget.show() # 解析json生成view并添加 if hasattr(self, "previewView"): # 删除旧view self.previewView.setParent(None) self.previewView.setVisible(False) self.previewView.hide() self.previewView.deleteLater() del self.previewView self.previewView = self.getChartView(text) if isinstance(self.previewView, QChartView): self.splitter.addWidget(self.previewView) else: QMessageBox.warning(self, "提示", self.previewView) del self.previewView self.setEnabled(True) self.loadingWidget.close() def getChartView(self, text): try: return ChartView(text) except Exception as e: return str(e) def resizeEvent(self, event): super(ChartWidget, self).resizeEvent(event) self.loadingWidget.resize(self.size()) def closeEvent(self, event): self.loadingWidget.close() self.loadingWidget.deleteLater() del self.loadingWidget super(ChartWidget, self).closeEvent(event)
class MainWindow(QMainWindow): def __init__(self, settings): super().__init__() # Save the settings object for now and shutdown time. self.settings = settings # Initialize extras and dicts paths first, as other modules use them paths.initialize(settings) # Initialize our font db fonts.initialize(settings) # Set our font, which will propogate to our child widgets. fonts.notify_me(self._font_change) # ask for a signal self._font_change(False) # fake a signal now # Initialize the dictionary apparatus dictionaries.initialize(settings) # Initialize the color choices colors.initialize(settings) # Initialize the sequence number for opened files self.book_number = 0 # Initialize the path to the last-opened file, used to # start file-open dialogs. self.last_open_path = '.' # Initialize our dict of active panels self.panel_dict = PANEL_DICT.copy() # Initialize our dict of open documents {seqno:Book} self.open_books = {} self.focus_book = None # seqno of book in focus, see _focus_me # Initialize the list of recent files self.recent_files = [] # Create the main window and set up the menus. self._uic() # Initialize the set of files actually open when we shut down. last_session = self._read_flist('mainwindow/open_files') if len(last_session) : # there were some files open if len(last_session) == 1 : msg = _TR('Start-up dialog', 'One book was open at the end of the last session.') else: msg = _TR('Start-up dialog', '%n books were open at the end of the last session.', n=len(last_session) ) info = _TR("Start-up dialog", "Click OK to re-open all") if utilities.ok_cancel_msg( msg, info) : for file_path in last_session : ftbs = utilities.path_to_stream(file_path) if ftbs : self._open(ftbs) if 0 == len(self.open_books) : # We did not re-open any books, either because there were # none, or the user said No, or perhaps they were not found. self._new() # open one, new, book. # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Slot to receive the currentChanged signal from the editview tabset. # Look through self.open_books and find the one whose edit widget is # now current, and do a focus_me for it. def _editview_change(self, index): if index > -1 : eview = self.editview_tabset.widget(index) for (seqno, book) in self.open_books.items() : if eview == book.get_edit_view() : self.focus_me(seqno) return mainwindow_logger.error('cannot relate editview tab index to book') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Make a selected book the focus of all panels. This is called explicitly # when a book is first created, and when the editview tabset changes the # current selection. It is called also when an editview gets a focus-in # event. # Display that Book's various "-view" objects in panels, in the order # that the user left them and with the same active panel as before. Note # that a book (editview) can get a focus-in event when it was already the # focus in this sense, for example if this app was hidden and then # brought to the front. So be prepared for redundant calls. def focus_me(self, book_index): outgoing = self.focus_book if book_index == outgoing : return # redundant call mainwindow_logger.debug( 'focusing {0} = {1}'.format(book_index,self.open_books[book_index].get_book_name()) ) self.focus_book = book_index # Record the user's arrangement of panels for the outgoing book, # as a list of tuples ('tabname', widget) in correct sequence. if outgoing is not None : # false first time and after File>Close out_panel_dict = self.open_books[outgoing].panel_dict widg_list = [] for ix in range( self.panel_tabset.count() ): widg_list.append ( (self.panel_tabset.tabText(ix), self.panel_tabset.widget(ix)) ) out_panel_dict['tab_list'] = widg_list out_panel_dict['current'] = self.panel_tabset.currentIndex() # Change all the panels to the widgets, in the sequence, of the new book in_panel_dict = self.open_books[book_index].panel_dict widg_list = in_panel_dict['tab_list'] self.panel_tabset.clear() for ix in range( len(widg_list) ): (tab_text, widget) = widg_list[ix] self.panel_tabset.insertTab(ix, widget, tab_text) self.panel_tabset.setCurrentIndex(in_panel_dict['current']) self.editview_tabset.setCurrentIndex( self.editview_tabset.indexOf( self.open_books[book_index].get_edit_view() ) ) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Called by the current book to make a particular tab the visible one, # e.g. to make the Find visible on a ^F. The argument is a widget # that should occupy one of the current tabs. Ask the tabset for its # index, and if it is found, make that the current index. (If it is # not found, log it and do nothing.) def make_tab_visible(self, tabwidg): ix = self.panel_tabset.indexOf(tabwidg) if ix >= 0 : # widget exists in this tabset self.panel_tabset.setCurrentIndex(ix) return mainwindow_logger.error('Request to show nonexistent widget') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement File>New: # Create a Book object # Call its new_empty() method, # Add it to the open_books dict keyed by its sequence number, # Display its text editor in a tab with the document name, and # Give it the focus. def _new(self): seq = self.book_number self.book_number += 1 new_book = book.Book( seq, self ) new_book.new_empty() self.open_books[seq] = new_book index = self.editview_tabset.addTab( new_book.get_edit_view(), new_book.get_book_name() ) self.editview_tabset.setTabToolTip(index, _TR('Tooltip of edit of new unsaved file', 'this file has not been saved') ) self.focus_me(seq) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Quick check to see if a file path is already open. Called from _open # and from _build_recent (menu). Returned value is the sequence number # of the open book, or None. def _is_already_open(self, path): for (seq, book_object) in self.open_books.items(): if path == book_object.get_book_full_path() : return seq return None # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement File>Open. Dialog with the user (file dialog starts with # last-used book path). Result is None or a FileBasedTextStream that we # pass to _open(). def _file_open(self) : fbts = utilities.ask_existing_file( _TR( 'File:Open dialog','Select a book file to open'), parent=self, starting_path=self.last_open_path) if fbts : # yes a readable file was chosen. self._open( fbts ) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Open a file, given the document as a FileBasedTextStream # * If file opened is fname.meta, look for a file named fname; if it # exists open it instead, e.g. given foo.txt.meta, open foo.txt. # If it doesn't exist, tell the user and exit. # * If a file of the same name and path is already open, just focus # it and exit. # * Determine if there is a .meta file, a .bin file, or neither # * Create a metadata input stream if possible # * If no .meta, look for good_words and bad_words # * If the only open book is an "Untitled-n" and # it is unmodified, delete it. # * Call Book.old_book() or .new_book() as appropriate # * Add this book's editview to the edit tabset # * Give this book the focus. def _open(self, fbts): # look for opening a .meta file if 'meta' == fbts.suffix(): fb2 = utilities.file_less_suffix(fbts) if fb2 is None : m1 = _TR('File:Open','Cannot open a .meta file alone') m2 = _TR('File:Open','There is no book file matching ', 'filename follows this') + fbts.filename() utilities.warning_msg(m1, m2) return # we see foo.txt with foo.txt.meta, silently open it fbts = fb2 # look for already-open file seq = self._is_already_open(fbts.fullpath()) if seq is not None : self.focus_me(seq) return # start collecting auxiliary streams gw_stream = None bw_stream = None gg_stream = None # open the metadata stream, which is always UTF-8 meta_stream = utilities.related_suffix(fbts, 'meta', encoding=C.ENCODING_UTF) if meta_stream is None : # opening book without .meta; look for .bin which is always LTN1 bin_stream = utilities.related_suffix(fbts,'bin',encoding=C.ENCODING_LATIN) if bin_stream : gg_stream = metadata.translate_bin(bin_stream,fbts) # Look for good_words.txt, bad_words.txt. gw_stream = utilities.related_file( fbts, 'good_words*.*' ) bw_stream = utilities.related_file( fbts, 'bad_words*.*' ) seq = self.book_number # If the only open book is the new one created at startup or when all # books are closed (which will have key 0), and it has not been # modified, get rid of it. if len(self.open_books) == 1 \ and 0 == list(self.open_books.keys())[0] \ and self.open_books[0].get_book_name().startswith('Untitled-') \ and not self.open_books[0].get_save_needed() : self.editview_tabset.clear() self.panel_tabset.clear() self.focus_book = None seq = 0 else: # Some other book open, or user typed into the default New one. self.book_number += 1 # Make the Book object and stow it in our open book dict a_book = book.Book( seq, self ) self.open_books[seq] = a_book if meta_stream : # opening a book we previously saved a_book.old_book( fbts, meta_stream ) else : a_book.new_book( fbts, gg_stream, gw_stream, bw_stream ) index = self.editview_tabset.addTab( a_book.get_edit_view(), a_book.get_book_name()) self.editview_tabset.setTabToolTip(index, a_book.get_book_folder() ) self.focus_me(seq) self.last_open_path = fbts.folderpath() # start for next open or save self._add_to_recent(fbts.fullpath()) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Save the book that is currently in focus under its present name, if it # is modified. Return True if the save completed, else False. # If the active book is a New one, force a Save-As action instead. def _save(self): active_book = self.open_books[self.focus_book] if active_book.get_save_needed() : if active_book.get_book_name().startswith('Untitled-'): return self._save_as() doc_stream = utilities.path_to_output( active_book.get_book_full_path() ) if doc_stream : # successfully opened for output meta_stream = utilities.related_output(doc_stream,'meta') if not meta_stream: utilities.warning_msg( _TR('File:Save', 'Unable to open metadata file for writing.'), _TR('File:Save', 'Use loglevel=error for details.') ) return False else: utilities.warning_msg( _TR('File:Save', 'Unable to open book file for writing.'), _TR('File:Save', 'Use loglevel=error for details.') ) return False return active_book.save_book(doc_stream, meta_stream) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement Save As. Query the user for a file path and get that as an # output FileBasedTextStream. Call the book to rename itself, which makes # it modified. Change the text in the edit tab to match. Discard the FBTS # and call _save which will make another one. def _save_as(self): active_book = self.open_books[self.focus_book] fbts = utilities.ask_saving_file( _TR('File:Save As dialog', 'Choose a new location and filename for this book' ), self, active_book.get_book_folder() ) if fbts : active_book.rename_book(fbts) self.editview_tabset.setTabText( self.editview_tabset.currentIndex(), fbts.filename() ) self._add_to_recent(fbts.fullpath()) fbts = None # discard that object return self._save() else: return False # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement Close. If the active book is modified, ask if it should # be saved. If it is 'Untitled-' that will turn into Save As. def _close(self): target_index = self.focus_book # active edit tab is to close target_book = self.open_books[target_index] if target_book.get_save_needed() : # Compose message of translated parts because _TR does not # allow for incorporating strings, only numbers. msg = _TR('File Close dialog', 'Book file ', 'filename follows here') msg += target_book.get_book_name() msg += _TR('File Close dialog', ' has been modified!', 'filename precedes this') ret = utilities.save_discard_cancel_msg( msg, info = _TR('File Close dialog', 'Save it, Discard changes, or Cancel Closing?') ) if ret is None : # Cancel return if ret : # True==Save self._save() # Now, get rid of the active book in 3 steps, # 1, close the book's tab in the editview tabset. We don't know which # tab it is, because the user can drag tabs around. i = self.editview_tabset.indexOf(target_book.get_edit_view()) # The following causes another tab to be focussed, changing self.focus_book # and saving target_book's tabs in target_book, not that we care. self.editview_tabset.removeTab(i) # 2, remove the book from our dict of open books. del self.open_books[target_index] # 3, if there are any open books remaining, the tab widget has # activated one of them by its rules, which caused a show signal and # entry to _focus_me already. However if there are no remaining books # there was no show signal or focus_me and the closed book's panels # are still in the tabset. if 0 == len(self.open_books) : self.book_number = 0 # restart the sequence self.focus_book = None self._new() # One way or the other, a focus_me has removed all references to # active_book's view panels except those in its PANEL_DICT. So the # following assignment should remove the last reference to the book, # and schedule the book and associated objects for garbage collect. target_book = None # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement loading and saving find panel user buttons. Start the search # for files in the active book's folder. User can navigate to extras # if need be. def _find_save(self): target_book = self.open_books[self.focus_book] find_panel = target_book.get_find_panel() stream = utilities.ask_saving_file( _TR('File:Save Find Buttons open dialog', 'Choose file to contain find button definitions'), self, starting_path=target_book.get_book_full_path(), encoding='UTF-8') if stream : # is not None, file is open find_panel.user_button_output(stream) # else user hit cancel, forget it def _find_load(self): target_book = self.open_books[self.focus_book] find_panel = target_book.get_find_panel() stream = utilities.ask_existing_file( _TR('File:Load Find Buttons open dialog', 'Choose a file of find button definitions'), self, starting_path=target_book.get_book_full_path(), encoding='UTF-8') if stream :# is not None, we opened it find_panel.user_button_input(stream) target_book.metadata_modified(True,C.MD_MOD_FLAG) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Maintain the list of "recent" file paths. The list is kept in usage # order, so if a path is in the list now, delete it and then add it to # the front. Keep it at a max of 9 items by deleting the oldest if # necessary. def _add_to_recent(self, path): if path in self.recent_files : del self.recent_files[self.recent_files.index(path)] self.recent_files.insert(0,path) self.recent_files = self.recent_files[:9] # Upon the aboutToShow signal from the File menu, populate the Recent # submenu with a list of files, but only the ones that are currently # accessible. If one is on a volume (e.g. USB stick) and you unmount the # volume, the path should not appear in the menu until the volume is # mounted again. def _open_recent(self, path): fbts = utilities.path_to_stream(path) if fbts : self._open(fbts) def _build_recent(self): active_files = [] for path in self.recent_files: seq = self._is_already_open(path) if (seq is None) and utilities.file_is_accessible(path) : active_files.append( (utilities.file_split(path),path) ) if 0 == len(active_files): self.recent_menu.setEnabled(False) return self.recent_menu.setEnabled(True) self.recent_menu.clear() i = 1 for ((fname, folder), path) in active_files: act = self.recent_menu.addAction( '{0} {1} {2}'.format(i,fname,folder) ) act.triggered.connect( lambda: self._open_recent(path) ) i += 1 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # User has chosen a different font; if it is the general font, set # that here so it will propogate to our children. def _font_change(self, is_mono): if not is_mono: self.setFont(fonts.get_general()) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Create the UI contained within this QMainWindow object. This is a lean # main window indeed. We have no toolbar, no status bar, no dock, # nothing. Just a splitter with, on the left, a tabset for editviews, and # on the right, a scrollbar containing a tabset for panels. (Qt Designer # note: it is not possible to build this structure with the Designer. It # will not let you put the scroll area into the splitter.) # # TODO: create a custom QTabWidget using a custom QTabBar to implement # drag-out-of-tabset behavior, and use those here. def _uic(self): # Create the tabset that displays editviews self.editview_tabset = QTabWidget() self.editview_tabset.setMovable(True) # let user move tabs around self.editview_tabset.currentChanged.connect(self._editview_change) # Create the tabset that displays find, notes, help &etc. self.panel_tabset = QTabWidget() self.panel_tabset.setMovable(True) # Create the splitter that contains the above two parts. self.splitter = QSplitter(Qt.Horizontal, self) self.splitter.setChildrenCollapsible(False) # Give just a little margin to the left of the editor self.splitter.setContentsMargins(8,0,0,0) self.splitter.addWidget(self.editview_tabset) self.splitter.addWidget(self.panel_tabset) # Set that splitter as the main window's central (and only) widget self.setCentralWidget(self.splitter) # Populate the panel tabset with empty widgets just so there will # be tabs that _swap can reference. for key in self.panel_dict.keys(): widj = QWidget() self.panel_tabset.addTab(widj,key) self.panel_dict[key] = widj # Size and position ourself based on saved settings. self.move(self.settings.value("mainwindow/position", QPoint(50,50))) self.resize(self.settings.value("mainwindow/size", C.STARTUP_DEFAULT_SIZE)) self.splitter.restoreState( self.settings.value("mainwindow/splitter",C.STARTUP_DEFAULT_SPLITTER) ) # Store a reference to the application menubar. In Mac OS this # is a parentless menubar; other platforms it is the default. if C.PLATFORM_IS_MAC : self.menu_bar = QMenuBar() # parentless menu bar for Mac OS else : self.menu_bar = self.menuBar # refer to the default one set_menu_bar(self.menu_bar) # Create the File menu, located in our menu_bar. self.file_menu = self.menu_bar.addMenu(_TR('Menu name', '&File')) # Populate the File menu with actions. # File:New -> _new() work = self.file_menu.addAction( _TR('File menu command','&New') ) work.setShortcut(QKeySequence.New) work.setToolTip( _TR('File:New tooltip','Create a new, empty document') ) work.triggered.connect(self._new) # File:Open -> _file_open() work = self.file_menu.addAction( _TR('File menu command','&Open') ) work.setShortcut(QKeySequence.Open) work.setToolTip( _TR('File:Open tooltip','Open an existing book') ) work.triggered.connect(self._file_open) # File:Save -> _file_save() work = self.file_menu.addAction( _TR('File menu command', '&Save') ) work.setShortcut(QKeySequence.Save) work.setToolTip( _TR('File:Save tooltip','Save the active book') ) work.triggered.connect(self._save) # Save As -> _file_save_as() work = self.file_menu.addAction( _TR('File menu command', 'Save &As') ) work.setShortcut(QKeySequence.SaveAs) work.setToolTip( _TR('File:Save As tooltip','Save the active book under a new name') ) work.triggered.connect(self._save_as) # Close -> _close() work = self.file_menu.addAction( _TR('File menu command', 'Close') ) work.setShortcut(QKeySequence.Close) work.setToolTip( _TR('File:Close tooltip', 'Close the active book') ) work.triggered.connect(self._close) # Load Find Buttons -> _find_load() work = self.file_menu.addAction( _TR('File menu command', 'Load Find Buttons') ) work.setToolTip( _TR('File:Load Find Buttons tooltip', 'Load a file of definitions for the custom buttons in the Find panel' ) ) work.triggered.connect(self._find_load) # Save Find Buttons -> _find_save() work = self.file_menu.addAction( _TR('File menu command', 'Save Find Buttons') ) work.setToolTip( _TR('File:Save Find Buttons tooltip', 'Save definitions of the custom buttons in the Find panel' ) ) work.triggered.connect(self._find_save) # Open Recent gets a submenu that is added to the File menu. # The aboutToShow signal is connected to our _build_recent slot. self.recent_menu = QMenu( _TR('Sub-menu name', '&Recent Files') ) work = self.file_menu.addMenu( self.recent_menu ) work.setToolTip( _TR('File:Recent tooltip', 'List of recently-used files to open') ) self.file_menu.aboutToShow.connect(self._build_recent) # divider if not Mac if not C.PLATFORM_IS_MAC: self.file_menu.addSeparator() # TODO Preferences with the menu role that on mac, moves to the app menu # Quit with the menu role that moves it to the app menu work = QAction( _TR('Quit command','&Quit'), self ) work.setMenuRole(QAction.QuitRole) work.setShortcut(QKeySequence.Quit) work.triggered.connect(self.close) self.file_menu.addAction(work) # Initialize the list of "recent" files for the File sub-menu. # These files were not necessarily open at shutdown, just sometime # in the not too distant past. self.recent_files = self._read_flist('mainwindow/recent_files') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Functions related to shutdown and management of settings. # # Factor out the job of reading/writing a list of files in the settings. # Input is a settings array key string like 'mainwindow/recent_files' # Output is a possibly empty list of canonical-file-path strings. def _read_flist(self, array_key): f_list = [] f_count = self.settings.beginReadArray(array_key) for f in range(f_count): # which may be 0 self.settings.setArrayIndex(f) f_list.append( self.settings.value('filepath') ) self.settings.endArray() return f_list # Input is an array key and a possibly empty list of path strings def _write_flist(self, file_list, array_key): if len(file_list): self.settings.beginWriteArray( array_key, len(file_list) ) for f in range(len(file_list)) : self.settings.setArrayIndex( f ) self.settings.setValue( 'filepath',file_list[f] ) self.settings.endArray() # Reimplement QWidget.closeEvent in order to save any open files # and update the settings. def closeEvent(self, event): # If there are any unsaved books, ask the user if they should be # saved. If the answer is yes, try to do so. unsaved = [] for (seq, book_object) in self.open_books.items() : if book_object.get_save_needed() : unsaved.append(seq) if len(unsaved): if len(unsaved) == 1 : msg = _TR('Shutdown message', 'There is one unsaved file') else : msg = _TR('Shutdown message', 'There are %n unsaved files', n=len(unsaved)) ret = utilities.save_discard_cancel_msg( msg, _TR('Shutdown message', 'Save, Discard changes, or Cancel Quit?') ) if ret is None : # user wants to cancel shutdown event.ignore() return if ret : # User want to save. Focus each unsaved file and call _save. # For all but "Untitled-n" documents this will be silent. For # those, it will open a save-as dialog. We ignore the return # from this because we cannot distinguish between a cancelled # file-open dialog and a file write error. for seq in unsaved : self.focus_me(seq) self._save() # Clear the settings so that old values don't hang around self.settings.clear() # Tell the submodules to save their current global values. colors.shutdown(self.settings) fonts.shutdown(self.settings) dictionaries.shutdown(self.settings) paths.shutdown(self.settings) # Save the list of currently-open files in the settings, but do not # save any whose filename matches "Untitled-#" because that is an # unsaved New file (which the user chose not to save, above). open_paths = [] for (index, book_obj) in self.open_books.items() : if not book_obj.get_book_name().startswith('Untitled-'): open_paths.append( book_obj.get_book_full_path() ) self._write_flist( open_paths, 'mainwindow/open_files' ) # Save the list of "recent" files in the settings. self._write_flist(self.recent_files, 'mainwindow/recent_files') # Save this window's position and size and splitter state self.settings.setValue("mainwindow/size",self.size()) self.settings.setValue("mainwindow/position",self.pos()) self.settings.setValue("mainwindow/splitter",self.splitter.saveState()) # and that's it, we are done finished, over & out. event.accept()
class MainGUI(QMainWindow): """The main GUI for azimuthal integration.""" _root_dir = osp.dirname(osp.abspath(__file__)) start_sgn = pyqtSignal() stop_sgn = pyqtSignal() quit_sgn = pyqtSignal() _db = RedisConnection() _WIDTH, _HEIGHT = config['GUI_MAIN_GUI_SIZE'] def __init__(self, pause_ev, close_ev): """Initialization.""" super().__init__() self._pause_ev = pause_ev self._close_ev = close_ev self._input_update_ev = Event() self._input = MpInQueue(self._input_update_ev, pause_ev, close_ev) self._pulse_resolved = config["PULSE_RESOLVED"] self._require_geometry = config["REQUIRE_GEOMETRY"] self._queue = deque(maxlen=1) self.setAttribute(Qt.WA_DeleteOnClose) self.title = f"EXtra-foam {__version__} ({config['DETECTOR']})" self.setWindowTitle(self.title + " - main GUI") # ************************************************************* # Central widget # ************************************************************* self._ctrl_widgets = [] # book-keeping control widgets self._cw = QSplitter() self._cw.setChildrenCollapsible(False) self.setCentralWidget(self._cw) self._left_cw_container = QScrollArea() self._left_cw_container.setFrameShape(QFrame.NoFrame) self._left_cw = QTabWidget() self._right_cw_container = QScrollArea() self._right_cw_container.setFrameShape(QFrame.NoFrame) self._right_cw = QSplitter(Qt.Vertical) self._right_cw.setChildrenCollapsible(False) self._source_cw = self.createCtrlWidget(DataSourceWidget) self._extension_cw = self.createCtrlWidget(ExtensionCtrlWidget) self._ctrl_panel_cw = QTabWidget() self._analysis_cw = QWidget() self._statistics_cw = QWidget() self._util_panel_container = QWidget() self._util_panel_cw = QTabWidget() # ************************************************************* # Tool bar # Note: the order of '_addAction` affect the unittest!!! # ************************************************************* self._tool_bar = self.addToolBar("Control") # make icon a bit larger self._tool_bar.setIconSize(2 * self._tool_bar.iconSize()) self._start_at = self._addAction("Start bridge", "start.png") self._start_at.triggered.connect(self.onStart) self._stop_at = self._addAction("Stop bridge", "stop.png") self._stop_at.triggered.connect(self.onStop) self._stop_at.setEnabled(False) self._tool_bar.addSeparator() image_tool_at = self._addAction("Image tool", "image_tool.png") image_tool_at.triggered.connect(lambda: (self._image_tool.show( ), self._image_tool.activateWindow())) open_poi_window_at = self._addAction("Pulse-of-interest", "poi.png") open_poi_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, PulseOfInterestWindow)) if not self._pulse_resolved: open_poi_window_at.setEnabled(False) pump_probe_window_at = self._addAction("Pump-probe", "pump-probe.png") pump_probe_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, PumpProbeWindow)) open_correlation_window_at = self._addAction("Correlation", "correlation.png") open_correlation_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, CorrelationWindow)) open_histogram_window_at = self._addAction("Histogram", "histogram.png") open_histogram_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, HistogramWindow)) open_bin2d_window_at = self._addAction("Binning", "binning.png") open_bin2d_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, BinningWindow)) self._tool_bar.addSeparator() open_file_stream_window_at = self._addAction("File stream", "file_stream.png") open_file_stream_window_at.triggered.connect( lambda: self.onOpenSatelliteWindow(FileStreamWindow)) open_about_at = self._addAction("About EXtra-foam", "about.png") open_about_at.triggered.connect( lambda: self.onOpenSatelliteWindow(AboutWindow)) # ************************************************************* # Miscellaneous # ************************************************************* # book-keeping opened windows self._plot_windows = WeakKeyDictionary() self._satellite_windows = WeakKeyDictionary() self._gui_logger = GuiLogger(parent=self) logger.addHandler(self._gui_logger) self._analysis_setup_manager = AnalysisSetupManager() self._thread_logger = ThreadLoggerBridge() self.quit_sgn.connect(self._thread_logger.stop) self._thread_logger_t = QThread() self._thread_logger.moveToThread(self._thread_logger_t) self._thread_logger_t.started.connect(self._thread_logger.recv) self._thread_logger.connectToMainThread(self) # For real time plot self._running = False self._plot_timer = QTimer() self._plot_timer.timeout.connect(self.updateAll) # For checking the connection to the Redis server self._redis_timer = QTimer() self._redis_timer.timeout.connect(self.pingRedisServer) self.__redis_connection_fails = 0 self._mon_proxy = MonProxy() # ************************************************************* # control widgets # ************************************************************* # analysis control widgets self.analysis_ctrl_widget = self.createCtrlWidget(AnalysisCtrlWidget) self.fom_filter_ctrl_widget = self.createCtrlWidget( FomFilterCtrlWidget) # ************************************************************* # status bar # ************************************************************* # StatusBar to display topic name self.statusBar().showMessage(f"TOPIC: {config['TOPIC']}") self.statusBar().setStyleSheet("QStatusBar{font-weight:bold;}") # ImageToolWindow is treated differently since it is the second # control window. self._image_tool = ImageToolWindow( queue=self._queue, pulse_resolved=self._pulse_resolved, require_geometry=self._require_geometry, parent=self) self.initUI() self.initConnections() self.updateMetaData() self._analysis_setup_manager.onInit() self.setMinimumSize(640, 480) self.resize(self._WIDTH, self._HEIGHT) self.show() def createCtrlWidget(self, widget_class): widget = widget_class(pulse_resolved=self._pulse_resolved, require_geometry=self._require_geometry, parent=self) self._ctrl_widgets.append(widget) return widget def initUI(self): self.initLeftUI() self.initRightUI() self._cw.addWidget(self._left_cw_container) self._cw.addWidget(self._right_cw_container) self._cw.setSizes([self._WIDTH * 0.6, self._WIDTH * 0.4]) def initLeftUI(self): self._left_cw.setTabPosition(QTabWidget.TabPosition.West) self._left_cw.addTab(self._source_cw, "Data source") self._left_cw_container.setWidget(self._left_cw) self._left_cw_container.setWidgetResizable(True) self._left_cw.addTab(self._extension_cw, "Extension") def initRightUI(self): self.initCtrlUI() self.initUtilUI() self._right_cw.addWidget(self._ctrl_panel_cw) self._right_cw.addWidget(self._util_panel_container) self._right_cw_container.setWidget(self._right_cw) self._right_cw_container.setWidgetResizable(True) def initCtrlUI(self): self.initGeneralAnalysisUI() self._ctrl_panel_cw.addTab(self._analysis_cw, "General analysis setup") def initGeneralAnalysisUI(self): layout = QVBoxLayout() layout.addWidget(self.analysis_ctrl_widget) layout.addWidget(self.fom_filter_ctrl_widget) self._analysis_cw.setLayout(layout) def initUtilUI(self): self._util_panel_cw.addTab(self._gui_logger.widget, "Logger") self._util_panel_cw.addTab(self._analysis_setup_manager, "Analysis Setup Manager") self._util_panel_cw.setTabPosition(QTabWidget.TabPosition.South) layout = QVBoxLayout() layout.addWidget(self._util_panel_cw) self._util_panel_container.setLayout(layout) def initConnections(self): self._analysis_setup_manager.load_metadata_sgn.connect( self.loadMetaData) def connect_input_to_output(self, output): self._input.connect(output) @profiler("Update Plots", process_time=True) def updateAll(self): """Update all the plots in the main and child windows.""" if not self._running: return try: processed = self._input.get() self._queue.append(processed) except Empty: return # clear the previous plots no matter what comes next # for w in self._plot_windows.keys(): # w.reset() data = self._queue[0] self._image_tool.updateWidgetsF() for w in itertools.chain(self._plot_windows): try: w.updateWidgetsF() except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() logger.debug( repr(traceback.format_tb(exc_traceback)) + repr(e)) logger.error(f"[Update plots] {repr(e)}") logger.debug(f"Plot train with ID: {data.tid}") def pingRedisServer(self): try: self._db.ping() if self.__redis_connection_fails > 0: # Note: Indeed, we do not have mechanism to recover from # a Redis server crash. It is recommended to restart # Extra-foam if you encounter this situation. logger.info("Reconnect to the Redis server!") self.__redis_connection_fails = 0 except ConnectionError: self.__redis_connection_fails += 1 rest_attempts = config["REDIS_MAX_PING_ATTEMPTS"] - \ self.__redis_connection_fails if rest_attempts > 0: logger.warning(f"No response from the Redis server! Shut " f"down after {rest_attempts} attempts ...") else: logger.warning(f"No response from the Redis server! " f"Shutting down!") self.close() def _addAction(self, description, filename): icon = QIcon(osp.join(self._root_dir, "icons/" + filename)) action = QAction(icon, description, self) self._tool_bar.addAction(action) return action def onOpenPlotWindow(self, instance_type): """Open a plot window if it does not exist. Otherwise bring the opened window to the table top. """ if self.checkWindowExistence(instance_type, self._plot_windows): return return instance_type(self._queue, pulse_resolved=self._pulse_resolved, require_geometry=self._require_geometry, parent=self) def onOpenSatelliteWindow(self, instance_type): """Open a satellite window if it does not exist. Otherwise bring the opened window to the table top. """ if self.checkWindowExistence(instance_type, self._satellite_windows): return return instance_type(parent=self) def checkWindowExistence(self, instance_type, windows): for key in windows: if isinstance(key, instance_type): key.activateWindow() return True return False def registerWindow(self, instance): self._plot_windows[instance] = 1 def unregisterWindow(self, instance): del self._plot_windows[instance] def registerSatelliteWindow(self, instance): self._satellite_windows[instance] = 1 def unregisterSatelliteWindow(self, instance): del self._satellite_windows[instance] @property def input(self): return self._input def start(self): """Start running. ProcessWorker interface. """ self._thread_logger_t.start() self._plot_timer.start(config["GUI_PLOT_UPDATE_TIMER"]) self._redis_timer.start(config["REDIS_PING_ATTEMPT_INTERVAL"]) self._input.start() def onStart(self): if not self.updateMetaData(): return self.start_sgn.emit() self._start_at.setEnabled(False) self._stop_at.setEnabled(True) for widget in self._ctrl_widgets: widget.onStart() for win in self._plot_windows: win.onStart() self._image_tool.onStart() self._analysis_setup_manager.onStart() self._running = True # starting to update plots self._input_update_ev.set() # notify update def onStop(self): """Actions taken before the end of a 'run'.""" self._running = False self.stop_sgn.emit() # TODO: wait for some signal self._start_at.setEnabled(True) self._stop_at.setEnabled(False) for widget in self._ctrl_widgets: widget.onStop() for win in self._plot_windows: win.onStop() self._image_tool.onStop() self._analysis_setup_manager.onStop() def updateMetaData(self): """Update metadata from all the ctrl widgets. :returns bool: True if all metadata successfully parsed and emitted, otherwise False. """ for widget in self._ctrl_widgets: if not widget.updateMetaData(): return False for win in self._plot_windows: if not win.updateMetaData(): return False return self._image_tool.updateMetaData() def loadMetaData(self): """Load metadata from Redis and set child control widgets.""" for widget in self._ctrl_widgets: widget.loadMetaData() for win in self._plot_windows: win.loadMetaData() self._image_tool.loadMetaData() @pyqtSlot(str, str) def onLogMsgReceived(self, ch, msg): if ch == 'log:debug': logger.debug(msg) elif ch == 'log:info': logger.info(msg) elif ch == 'log:warning': logger.warning(msg) elif ch == 'log:error': logger.error(msg) def closeEvent(self, QCloseEvent): # prevent from logging in the GUI when it has been closed logger.removeHandler(self._gui_logger) # tell all processes to close self._close_ev.set() # clean up the logger thread self.quit_sgn.emit() self._thread_logger_t.quit() self._thread_logger_t.wait() # shutdown pipeline workers and Redis server shutdown_all() self._image_tool.close() for window in list( itertools.chain(self._plot_windows, self._satellite_windows)): # Close all open child windows to make sure their resources # (any running process etc.) are released gracefully. This # is especially necessary for the case when file stream was # still ongoing when the main GUI was closed. window.close() super().closeEvent(QCloseEvent)
class Listspace(QSplitter, ViewManager): """ Class implementing the listspace viewmanager class. @signal changeCaption(str) emitted if a change of the caption is necessary @signal editorChanged(str) emitted when the current editor has changed @signal editorChangedEd(Editor) emitted when the current editor has changed @signal lastEditorClosed() emitted after the last editor window was closed @signal editorOpened(str) emitted after an editor window was opened @signal editorOpenedEd(Editor) emitted after an editor window was opened @signal editorClosed(str) emitted just before an editor window gets closed @signal editorClosedEd(Editor) emitted just before an editor window gets closed @signal editorRenamed(str) emitted after an editor was renamed @signal editorRenamedEd(Editor) emitted after an editor was renamed @signal editorSaved(str) emitted after an editor window was saved @signal editorSavedEd(Editor) emitted after an editor window was saved @signal checkActions(Editor) emitted when some actions should be checked for their status @signal cursorChanged(Editor) emitted after the cursor position of the active window has changed @signal breakpointToggled(Editor) emitted when a breakpoint is toggled. @signal bookmarkToggled(Editor) emitted when a bookmark is toggled. @signal syntaxerrorToggled(Editor) emitted when a syntax error is toggled. @signal previewStateChanged(bool) emitted to signal a change in the preview state @signal editorLanguageChanged(Editor) emitted to signal a change of an editors language @signal editorTextChanged(Editor) emitted to signal a change of an editor's text @signal editorLineChanged(str,int) emitted to signal a change of an editor's current line (line is given one based) """ changeCaption = pyqtSignal(str) editorChanged = pyqtSignal(str) editorChangedEd = pyqtSignal(Editor) lastEditorClosed = pyqtSignal() editorOpened = pyqtSignal(str) editorOpenedEd = pyqtSignal(Editor) editorClosed = pyqtSignal(str) editorClosedEd = pyqtSignal(Editor) editorRenamed = pyqtSignal(str) editorRenamedEd = pyqtSignal(Editor) editorSaved = pyqtSignal(str) editorSavedEd = pyqtSignal(Editor) checkActions = pyqtSignal(Editor) cursorChanged = pyqtSignal(Editor) breakpointToggled = pyqtSignal(Editor) bookmarkToggled = pyqtSignal(Editor) syntaxerrorToggled = pyqtSignal(Editor) previewStateChanged = pyqtSignal(bool) editorLanguageChanged = pyqtSignal(Editor) editorTextChanged = pyqtSignal(Editor) editorLineChanged = pyqtSignal(str, int) def __init__(self, parent): """ Constructor @param parent parent widget (QWidget) """ self.stacks = [] QSplitter.__init__(self, parent) ViewManager.__init__(self) self.setChildrenCollapsible(False) self.viewlist = QListWidget(self) policy = self.viewlist.sizePolicy() policy.setHorizontalPolicy(QSizePolicy.Ignored) self.viewlist.setSizePolicy(policy) self.addWidget(self.viewlist) self.viewlist.setContextMenuPolicy(Qt.CustomContextMenu) self.viewlist.currentRowChanged.connect(self.__showSelectedView) self.viewlist.customContextMenuRequested.connect(self.__showMenu) self.stackArea = QSplitter(self) self.stackArea.setChildrenCollapsible(False) self.addWidget(self.stackArea) self.stackArea.setOrientation(Qt.Vertical) stack = StackedWidget(self.stackArea) self.stackArea.addWidget(stack) self.stacks.append(stack) self.currentStack = stack stack.currentChanged.connect(self.__currentChanged) stack.installEventFilter(self) self.setSizes([int(self.width() * 0.2), int(self.width() * 0.8)]) # 20% for viewlist, 80% for the editors self.__inRemoveView = False self.__initMenu() self.contextMenuEditor = None self.contextMenuIndex = -1 def __initMenu(self): """ Private method to initialize the viewlist context menu. """ self.__menu = QMenu(self) self.__menu.addAction(UI.PixmapCache.getIcon("tabClose.png"), self.tr('Close'), self.__contextMenuClose) self.closeOthersMenuAct = self.__menu.addAction( UI.PixmapCache.getIcon("tabCloseOther.png"), self.tr("Close Others"), self.__contextMenuCloseOthers) self.__menu.addAction(self.tr('Close All'), self.__contextMenuCloseAll) self.__menu.addSeparator() self.saveMenuAct = self.__menu.addAction( UI.PixmapCache.getIcon("fileSave.png"), self.tr('Save'), self.__contextMenuSave) self.__menu.addAction(UI.PixmapCache.getIcon("fileSaveAs.png"), self.tr('Save As...'), self.__contextMenuSaveAs) self.__menu.addAction(UI.PixmapCache.getIcon("fileSaveAll.png"), self.tr('Save All'), self.__contextMenuSaveAll) self.__menu.addSeparator() self.openRejectionsMenuAct = self.__menu.addAction( self.tr("Open 'rejection' file"), self.__contextMenuOpenRejections) self.__menu.addSeparator() self.__menu.addAction(UI.PixmapCache.getIcon("print.png"), self.tr('Print'), self.__contextMenuPrintFile) self.__menu.addSeparator() self.copyPathAct = self.__menu.addAction( self.tr("Copy Path to Clipboard"), self.__contextMenuCopyPathToClipboard) def __showMenu(self, point): """ Private slot to handle the customContextMenuRequested signal of the viewlist. @param point position to open the menu at (QPoint) """ if self.editors: itm = self.viewlist.itemAt(point) if itm is not None: row = self.viewlist.row(itm) self.contextMenuEditor = self.editors[row] self.contextMenuIndex = row if self.contextMenuEditor: self.saveMenuAct.setEnabled( self.contextMenuEditor.isModified()) fileName = self.contextMenuEditor.getFileName() self.copyPathAct.setEnabled(bool(fileName)) if fileName: rej = "{0}.rej".format(fileName) self.openRejectionsMenuAct.setEnabled( os.path.exists(rej)) else: self.openRejectionsMenuAct.setEnabled(False) self.closeOthersMenuAct.setEnabled( self.viewlist.count() > 1) self.__menu.popup(self.viewlist.mapToGlobal(point)) def canCascade(self): """ Public method to signal if cascading of managed windows is available. @return flag indicating cascading of windows is available """ return False def canTile(self): """ Public method to signal if tiling of managed windows is available. @return flag indicating tiling of windows is available """ return False def canSplit(self): """ public method to signal if splitting of the view is available. @return flag indicating splitting of the view is available. """ return True def tile(self): """ Public method to tile the managed windows. """ pass def cascade(self): """ Public method to cascade the managed windows. """ pass def _removeAllViews(self): """ Protected method to remove all views (i.e. windows). """ self.viewlist.clear() for win in self.editors: for stack in self.stacks: if stack.hasEditor(win): stack.removeWidget(win) break win.closeIt() def _removeView(self, win): """ Protected method to remove a view (i.e. window). @param win editor window to be removed """ self.__inRemoveView = True ind = self.editors.index(win) itm = self.viewlist.takeItem(ind) if itm: del itm for stack in self.stacks: if stack.hasEditor(win): stack.removeWidget(win) break win.closeIt() self.__inRemoveView = False if ind > 0: ind -= 1 else: if len(self.editors) > 1: ind = 1 else: return stack.setCurrentWidget(stack.firstEditor()) self._showView(self.editors[ind].parent()) aw = self.activeWindow() fn = aw and aw.getFileName() or None if fn: self.changeCaption.emit(fn) self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, aw.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(aw) def _addView(self, win, fn=None, noName="", next=False): """ Protected method to add a view (i.e. window). @param win editor assembly to be added @param fn filename of this editor (string) @param noName name to be used for an unnamed editor (string) @param next flag indicating to add the view next to the current view (bool) """ editor = win.getEditor() if fn is None: if not noName: self.untitledCount += 1 noName = self.tr("Untitled {0}").format(self.untitledCount) self.viewlist.addItem(noName) editor.setNoName(noName) else: txt = os.path.basename(fn) if not QFileInfo(fn).isWritable(): txt = self.tr("{0} (ro)").format(txt) itm = QListWidgetItem(txt) itm.setToolTip(fn) self.viewlist.addItem(itm) self.currentStack.addWidget(win) self.currentStack.setCurrentWidget(win) editor.captionChanged.connect(self.__captionChange) editor.cursorLineChanged.connect(self.__cursorLineChanged) index = self.editors.index(editor) self.viewlist.setCurrentRow(index) editor.setFocus() if fn: self.changeCaption.emit(fn) self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(editor) def __captionChange(self, cap, editor): """ Private method to handle caption change signals from the editor. Updates the listwidget text to reflect the new caption information. @param cap Caption for the editor (string) @param editor Editor to update the caption for """ fn = editor.getFileName() if fn: self.setEditorName(editor, fn) def __cursorLineChanged(self, lineno): """ Private slot to handle a change of the current editor's cursor line. @param lineno line number of the current editor's cursor (zero based) """ editor = self.sender() if editor: fn = editor.getFileName() if fn: self.editorLineChanged.emit(fn, lineno + 1) def _showView(self, win, fn=None): """ Protected method to show a view (i.e. window). @param win editor assembly to be shown @param fn filename of this editor (string) """ editor = win.getEditor() for stack in self.stacks: if stack.hasEditor(editor): stack.setCurrentWidget(win) self.currentStack = stack break index = self.editors.index(editor) self.viewlist.setCurrentRow(index) editor.setFocus() fn = editor.getFileName() if fn: self.changeCaption.emit(fn) self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(editor) def __showSelectedView(self, row): """ Private slot called to show a view selected in the list. @param row row number of the item clicked on (integer) """ if row != -1: self._showView(self.editors[row].parent()) self._checkActions(self.editors[row]) def activeWindow(self): """ Public method to return the active (i.e. current) window. @return reference to the active editor """ return self.currentStack.currentWidget() def showWindowMenu(self, windowMenu): """ Public method to set up the viewmanager part of the Window menu. @param windowMenu reference to the window menu """ pass def _initWindowActions(self): """ Protected method to define the user interface actions for window handling. """ pass def setEditorName(self, editor, newName): """ Public method to change the displayed name of the editor. @param editor editor window to be changed @param newName new name to be shown (string) """ if newName: currentRow = self.viewlist.currentRow() index = self.editors.index(editor) txt = os.path.basename(newName) if not QFileInfo(newName).isWritable(): txt = self.tr("{0} (ro)").format(txt) itm = self.viewlist.item(index) itm.setText(txt) itm.setToolTip(newName) self.viewlist.setCurrentRow(currentRow) self.changeCaption.emit(newName) def _modificationStatusChanged(self, m, editor): """ Protected slot to handle the modificationStatusChanged signal. @param m flag indicating the modification status (boolean) @param editor editor window changed """ currentRow = self.viewlist.currentRow() index = self.editors.index(editor) keys = [] if m: keys.append("fileModified.png") if editor.hasSyntaxErrors(): keys.append("syntaxError22.png") elif editor.hasWarnings(): keys.append("warning22.png") if not keys: keys.append("empty.png") self.viewlist.item(index).setIcon(UI.PixmapCache.getCombinedIcon(keys)) self.viewlist.setCurrentRow(currentRow) self._checkActions(editor) def _syntaxErrorToggled(self, editor): """ Protected slot to handle the syntaxerrorToggled signal. @param editor editor that sent the signal """ currentRow = self.viewlist.currentRow() index = self.editors.index(editor) keys = [] if editor.isModified(): keys.append("fileModified.png") if editor.hasSyntaxErrors(): keys.append("syntaxError22.png") elif editor.hasWarnings(): keys.append("warning22.png") if not keys: keys.append("empty.png") self.viewlist.item(index).setIcon(UI.PixmapCache.getCombinedIcon(keys)) self.viewlist.setCurrentRow(currentRow) ViewManager._syntaxErrorToggled(self, editor) def addSplit(self): """ Public method used to split the current view. """ stack = StackedWidget(self.stackArea) stack.show() self.stackArea.addWidget(stack) self.stacks.append(stack) self.currentStack = stack stack.currentChanged.connect(self.__currentChanged) stack.installEventFilter(self) if self.stackArea.orientation() == Qt.Horizontal: size = self.stackArea.width() else: size = self.stackArea.height() self.stackArea.setSizes([int(size / len(self.stacks))] * len(self.stacks)) self.splitRemoveAct.setEnabled(True) self.nextSplitAct.setEnabled(True) self.prevSplitAct.setEnabled(True) def removeSplit(self): """ Public method used to remove the current split view. @return flag indicating successfull removal """ if len(self.stacks) > 1: stack = self.currentStack res = True savedEditors = stack.editors[:] for editor in savedEditors: res &= self.closeEditor(editor) if res: try: i = self.stacks.index(stack) except ValueError: return True if i == len(self.stacks) - 1: i -= 1 self.stacks.remove(stack) stack.close() self.currentStack = self.stacks[i] if len(self.stacks) == 1: self.splitRemoveAct.setEnabled(False) self.nextSplitAct.setEnabled(False) self.prevSplitAct.setEnabled(False) return True return False def getSplitOrientation(self): """ Public method to get the orientation of the split view. @return orientation of the split (Qt.Horizontal or Qt.Vertical) """ return self.stackArea.orientation() def setSplitOrientation(self, orientation): """ Public method used to set the orientation of the split view. @param orientation orientation of the split (Qt.Horizontal or Qt.Vertical) """ self.stackArea.setOrientation(orientation) def nextSplit(self): """ Public slot used to move to the next split. """ aw = self.activeWindow() _hasFocus = aw and aw.hasFocus() ind = self.stacks.index(self.currentStack) + 1 if ind == len(self.stacks): ind = 0 self.currentStack = self.stacks[ind] if _hasFocus: aw = self.activeWindow() if aw: aw.setFocus() index = self.editors.index(self.currentStack.currentWidget()) self.viewlist.setCurrentRow(index) def prevSplit(self): """ Public slot used to move to the previous split. """ aw = self.activeWindow() _hasFocus = aw and aw.hasFocus() ind = self.stacks.index(self.currentStack) - 1 if ind == -1: ind = len(self.stacks) - 1 self.currentStack = self.stacks[ind] if _hasFocus: aw = self.activeWindow() if aw: aw.setFocus() index = self.editors.index(self.currentStack.currentWidget()) self.viewlist.setCurrentRow(index) def __contextMenuClose(self): """ Private method to close the selected editor. """ if self.contextMenuEditor: self.closeEditorWindow(self.contextMenuEditor) def __contextMenuCloseOthers(self): """ Private method to close the other editors. """ index = self.contextMenuIndex for i in list(range(self.viewlist.count() - 1, index, -1)) + \ list(range(index - 1, -1, -1)): editor = self.editors[i] self.closeEditorWindow(editor) def __contextMenuCloseAll(self): """ Private method to close all editors. """ savedEditors = self.editors[:] for editor in savedEditors: self.closeEditorWindow(editor) def __contextMenuSave(self): """ Private method to save the selected editor. """ if self.contextMenuEditor: self.saveEditorEd(self.contextMenuEditor) def __contextMenuSaveAs(self): """ Private method to save the selected editor to a new file. """ if self.contextMenuEditor: self.saveAsEditorEd(self.contextMenuEditor) def __contextMenuSaveAll(self): """ Private method to save all editors. """ self.saveEditorsList(self.editors) def __contextMenuOpenRejections(self): """ Private slot to open a rejections file associated with the selected editor. """ if self.contextMenuEditor: fileName = self.contextMenuEditor.getFileName() if fileName: rej = "{0}.rej".format(fileName) if os.path.exists(rej): self.openSourceFile(rej) def __contextMenuPrintFile(self): """ Private method to print the selected editor. """ if self.contextMenuEditor: self.printEditor(self.contextMenuEditor) def __contextMenuCopyPathToClipboard(self): """ Private method to copy the file name of the selected editor to the clipboard. """ if self.contextMenuEditor: fn = self.contextMenuEditor.getFileName() if fn: cb = QApplication.clipboard() cb.setText(fn) def __currentChanged(self, index): """ Private slot to handle the currentChanged signal. @param index index of the current editor """ if index == -1 or not self.editors: return editor = self.activeWindow() if editor is None: return self._checkActions(editor) editor.setFocus() fn = editor.getFileName() if fn: self.changeCaption.emit(fn) if not self.__inRemoveView: self.editorChanged.emit(fn) self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(editor) cindex = self.editors.index(editor) self.viewlist.setCurrentRow(cindex) def eventFilter(self, watched, event): """ Public method called to filter the event queue. @param watched the QObject being watched @param event the event that occurred @return flag indicating, if we handled the event """ if event.type() == QEvent.MouseButtonPress and \ not event.button() == Qt.RightButton: switched = True if isinstance(watched, QStackedWidget): switched = watched is not self.currentStack self.currentStack = watched elif isinstance(watched, QScintilla.Editor.Editor): for stack in self.stacks: if stack.hasEditor(watched): switched = stack is not self.currentStack self.currentStack = stack break currentWidget = self.currentStack.currentWidget() if currentWidget: index = self.editors.index(currentWidget) self.viewlist.setCurrentRow(index) aw = self.activeWindow() if aw is not None: self._checkActions(aw) aw.setFocus() fn = aw.getFileName() if fn: self.changeCaption.emit(fn) if switched: self.editorChanged.emit(fn) self.editorLineChanged.emit( fn, aw.getCursorPosition()[0] + 1) else: self.changeCaption.emit("") self.editorChangedEd.emit(aw) return False
class UI_mainWindow(QMainWindow): __tlvInfoGB_ = None __tlvPkgListGB_ = None __videoGB_ = None __videoLabel_ = None __audioGB_ = None __audioLabel_ = None __menubar_ = None __winResolution_ = (1280, 720) def __init__(self): super().__init__() # self.setWindowFlag(Qt.FramelessWindowHint) self.__vsplitter_ = QSplitter(Qt.Vertical) self.__vsplitter_.setChildrenCollapsible(False) # 拉动分割器至最小,被分割部分不会消失 self.__vsplitter_.setAutoFillBackground(True) # 分割器随主窗口大小自适应变化 self.__hsplitter_ = QSplitter(Qt.Horizontal) self.__hsplitter_.setChildrenCollapsible(False) self.__hsplitter_.setAutoFillBackground(True) self.__initTlvInfoLayout() self.__initVideoLayout() self.__initAudioLayout() self.__initTlvPkgListLayout() self.__initMainWindow() def __initMainWindow(self): availGeometry = QDesktopWidget().availableGeometry() self.resize(availGeometry.width() * 0.7, availGeometry.height() * 0.7) self.__center() self.__menubar_ = UIMenuBar() self.setMenuBar(self.__menubar_) self.setCentralWidget(self.__hsplitter_) self.setWindowTitle('tlv分析工具') def __center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) def __initTlvInfoLayout(self): tlvinfo = UITlvInfo('TLV INFO') tlvinfo.setMinimumWidth(250) self.__hsplitter_.addWidget(tlvinfo) def __initTlvPkgListLayout(self): self.__tlvpkglist_ = UITlvPkgList('Tlv Pkg List') for i in range(30): pkg_level = UITlvPkgList.PKG_LEVEL.NORMAL if i % 10 == 0: pkg_level = UITlvPkgList.PKG_LEVEL.LOSS elif i % 11 == 0: pkg_level = UITlvPkgList.PKG_LEVEL.ORDER self.__tlvpkglist_.insertTlvPkgItem( "H264 RTP Header #{:<10d}".format(i), '''this is an example! this is an apple! this is a banana! this is a pen! end''', pkg_level) self.__hsplitter_.addWidget(self.__tlvpkglist_) def __initVideoLayout(self): label = UIVideoLabel() label.setMinimumSize(640, 480) hlayout = QHBoxLayout() hlayout.addStretch(1) hlayout.addWidget(label) hlayout.addStretch(1) vlayout = QVBoxLayout() # vlayout.addStretch(1) vlayout.addLayout(hlayout) # vlayout.addStretch(1) video = QGroupBox("Video Replay") video.setLayout(vlayout) self.__vsplitter_.addWidget(video) def __initAudioLayout(self): hlayout = QHBoxLayout() hlayout.addStretch(1) hlayout.addWidget(QLabel('Audio Replay')) hlayout.addStretch(1) audio = QGroupBox("Audio Replay") audio.setLayout(hlayout) self.__vsplitter_.addWidget(audio) self.__vsplitter_.setSizes([420, 300]) self.__hsplitter_.addWidget(self.__vsplitter_)
class CodecTab(QScrollArea): # BUG: codec_frame should have height 210 but has 480. # WORKAROUND: manually set height to 210 height. # SEE: https://forum.qt.io/topic/42055/qwidget-height-returns-incorrect-value-in-5-3/7 FRAME_HEIGHT = 210 def __init__(self, parent, context, commands): super(QWidget, self).__init__(parent) self._context = context self._logger = context.logger() self._commands = commands self._next_frame_id = 1 self._frames = QSplitter(Qt.Vertical) self._frames.setChildrenCollapsible(False) self._frames.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self._frames.setContentsMargins(0, 0, 0, 0) self._main_frame = QFrame(self) self._main_frame_layout = QVBoxLayout() self._main_frame_layout.addWidget(self._frames) self._main_frame_layout.addWidget(VSpacer(self)) self._main_frame.setLayout(self._main_frame_layout) self.newFrame("", "") self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setWidgetResizable(True) self.setWidget(self._main_frame) def newFrame(self, text, title, previous_frame=None, status=None, msg=None): try: # BUG: Setting complex default values is not possible in python # WORKAROUND: Set default value to None and set real default later. if status is None: status = StatusWidget.DEFAULT if previous_frame and previous_frame.hasNext(): next_frame = previous_frame.next() next_frame.setTitle(title) finished = False if status == StatusWidget.ERROR: while not finished: next_frame.flashStatus(status, msg) # Display error only for the first frame. msg = None finished = not next_frame.hasNext() next_frame = next_frame.next() else: next_frame.setInputText(text, msg is not None and len(msg) > 0) next_frame.flashStatus(status, msg) previous_frame.focusInputText() else: new_frame = CodecFrame(self, self._context, self._next_frame_id, self, self._commands, previous_frame, text) self._next_frame_id += 1 if self._frames.count() > 0: new_frame.flashStatus(status, msg) new_frame.setTitle(title) new_frame.setContentsMargins(0, 0, 0, 0) new_frame.layout().setContentsMargins(0, 0, 0, 0) self._frames.addWidget(new_frame) # BUG: QSplitter does not allow frames to be wider than the surrounding area (here: QScrollArea). # WORKAROUND: Set a fixed size for codec frames and disable handles which prevents users from # trying to resize the codec frames. new_frame.setFixedHeight(self.FRAME_HEIGHT) self._frames.handle(self._frames.count() - 1).setEnabled(False) if previous_frame: previous_frame.focusInputText() else: new_frame.focusInputText() except Exception as e: self._logger.error("Unknown error: {}".format(str(e))) def removeFrames(self, frame): if frame: if frame.previous(): frame.previous().setNext(None) frames_to_remove = [frame] while frame.next(): frames_to_remove.append(frame.next()) frame = frame.next() for frame_to_remove in reversed(frames_to_remove): frame_to_remove.deleteLater() def getFocussedFrame(self): widget = self._frames.focusWidget() while widget: if isinstance(widget, CodecFrame): return widget widget = widget.parent() return self._frames.widget(0)
class Ui_MainWindow(object): def _setupMenu(self, MainWindow): self.menubar = MainWindow.menuBar() self.menubar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.menuFile = QtWidgets.QMenu(self.menubar) self.menuFile.setObjectName("menuFile") self.menuhelp = QtWidgets.QMenu(self.menubar) self.menuhelp.setObjectName("menuhelp") MainWindow.setMenuBar(self.menubar) self.actionExit = QtWidgets.QAction() self.actionExit.setObjectName("actionExit") self.actionabout_version = QtWidgets.QAction() self.actionabout_version.setObjectName("actionabout_version") self.actionmanual = QtWidgets.QAction() self.actionmanual.setObjectName("actionmanual") self.menuFile.addAction(self.actionExit) self.menuhelp.addAction(self.actionabout_version) self.menuhelp.addAction(self.actionmanual) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuhelp.menuAction()) def setupUi(self, MainWindow): #主窗口 MainWindow.setObjectName("MainWindow") MainWindow.setEnabled(True) #菜单设置 self._setupMenu(MainWindow) self.centralwidget = QtWidgets.QWidget(MainWindow) MainWindow.setCentralWidget(self.centralwidget) QtCore.QMetaObject.connectSlotsByName(MainWindow) # 左侧功能面板 self.mainPannelTabWidget = QtWidgets.QTabWidget() #右侧日志命令窗口 self.logCommandTabWidget = QtWidgets.QTabWidget() self.logCommandTabWidget.setTabPosition(QtWidgets.QTabWidget.South) # 主功能面板功能细分 mysql、es、oracle self.mysqlQWidget = QtWidgets.QWidget() self.esQWidget = QtWidgets.QWidget() self.oracleQWidget = QtWidgets.QWidget() self.mainPannelTabWidget.addTab(self.mysqlQWidget, "") self.mainPannelTabWidget.addTab(self.esQWidget, "") self.mainPannelTabWidget.addTab(self.oracleQWidget, "") self.mainPannelTabWidget.setCurrentIndex(0) # 主框架 self.splitter = QSplitter(Qt.Horizontal) # 主框架组装 self.splitter.addWidget(self.mainPannelTabWidget) self.splitter.addWidget(self.logCommandTabWidget) self.splitter.setHandleWidth(1) self.splitter.setStretchFactor(0, 25) self.splitter.setStretchFactor(1, 75) self.splitter.setChildrenCollapsible(False) self.boxlayout = QVBoxLayout() self.boxlayout.setContentsMargins(0, 0, 0, 0) self.boxlayout.addWidget(self.splitter) self.centralwidget.setLayout(self.boxlayout) #文字显示 self.retranslateUi(MainWindow) mysql = MysqlWindow(self, self.mysqlQWidget, self.logCommandTabWidget) mysql.setupUi() es = ESWindow(self, self.esQWidget, self.logCommandTabWidget) es.setupUi() oracle = OracleWindow(self, self.oracleQWidget, self.logCommandTabWidget) oracle.setupUi() def retranslateUi(self, MainWindow): self._translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(self._translate("MainWindow", "Python运维工具")) self.mainPannelTabWidget.setToolTip( self._translate("MainWindow", "<html><head/><body><p>mysql</p></body></html>")) self.mainPannelTabWidget.setWhatsThis( self._translate("MainWindow", "<html><head/><body><p>MySQL</p></body></html>")) self.mainPannelTabWidget.setTabText( self.mainPannelTabWidget.indexOf(self.mysqlQWidget), self._translate("MainWindow", "MySQL")) self.mainPannelTabWidget.setTabText( self.mainPannelTabWidget.indexOf(self.esQWidget), self._translate("MainWindow", "ElasticSearch")) self.mainPannelTabWidget.setTabText( self.mainPannelTabWidget.indexOf(self.oracleQWidget), self._translate("MainWindow", "Oracle")) self.menuFile.setTitle(self._translate("MainWindow", "文件")) self.menuhelp.setTitle(self._translate("MainWindow", "帮助")) self.actionExit.setText(self._translate("MainWindow", "退出")) self.actionabout_version.setText(self._translate("MainWindow", "关于")) self.actionmanual.setText(self._translate("MainWindow", "手册"))
class Editor(QWidget): """ This class is the central widget of the MainWindow. It contains the items library, diagram graphics scene and graphics view, and the inspector widget Function of Connections: Logically: A Connection is composed of a fromPort and a toPort, which gives the direction of the pipe. Ports are attached to Blocks. Visually: A diagram editor has a QGraphicsLineItem (connLineItem) which is set Visible only when a connection is being created Function of BlockItems: Items can be added to the library by adding them to the model of the library broswer view. Then they can be dragged and dropped into the diagram view. Function of trnsysExport: When exporting the trnsys file, exportData() is called. Function of save and load: A diagram can be saved to a json file by calling encodeDiagram and can then be loaded by calling decodeDiagram wiht appropiate filenames. Attributes ---------- projectFolder : str Path to the folder of the project diagramName : str Name used for saving the diagram saveAsPath : :obj:`Path` Default saving location is trnsysGUI/diagrams, path only set if "save as" used idGen : :obj:`IdGenerator` Is used to distribute ids (id, trnsysId(for trnsysExport), etc) alignMode : bool Enables mode in which a dragged block is aligned to y or x value of another one Toggled in the MainWindow class in toggleAlignMode() editorMode : int Mode 0: Pipes are PolySun-like Mode 1: Pipes have only 90deg angles, visio-like snapGrid : bool Enable/Disable align grid snapSize : int Size of align grid horizontalLayout : :obj:`QHBoxLayout` Contains the diagram editor and the layout containing the library browser view and the listview vertL : :obj:`QVBoxLayout` Cointains the library browser view and the listWidget moveDirectPorts: bool Enables/Disables moving direct ports of storagetank (doesn't work with HxPorts yet) diagramScene : :obj:`QGraphicsScene` Contains the "logical" part of the diagram diagramView : :obj:`QGraphicsView` Contains the visualization of the diagramScene _currentlyDraggedConnectionFromPort : :obj:`PortItem` connectionList : :obj:`List` of :obj:`Connection` trnsysObj : :obj:`List` of :obj:`BlockItem` and :obj:`Connection` graphicalObj : :obj:`List` of :obj:`GraphicalItem` connLine : :obj:`QLineF` connLineItem = :obj:`QGraphicsLineItem` """ def __init__(self, parent, projectFolder, jsonPath, loadValue, logger): super().__init__(parent) self.logger = logger self.logger.info("Initializing the diagram editor") self.projectFolder = projectFolder self.diagramName = os.path.split(self.projectFolder)[-1] + ".json" self.saveAsPath = _pl.Path() self.idGen = IdGenerator() self.testEnabled = False self.existReference = True self.controlExists = 0 self.controlDirectory = "" self.alignMode = False self.moveDirectPorts = False self.editorMode = 1 # Related to the grid blocks can snap to self.snapGrid = False self.snapSize = 20 self.trnsysPath = _pl.Path(r"C:\Trnsys17\Exe\TRNExe.exe") self.horizontalLayout = QHBoxLayout(self) self.libraryBrowserView = QListView(self) self.libraryModel = LibraryModel(self) self.libraryBrowserView.setGridSize(QSize(65, 65)) self.libraryBrowserView.setResizeMode(QListView.Adjust) self.libraryModel.setColumnCount(0) componentNamesWithIcon = [ ("Connector", _img.CONNECTOR_SVG.icon()), ("TeePiece", _img.TEE_PIECE_SVG.icon()), ("DPTee", _img.DP_TEE_PIECE_SVG.icon()), ("SPCnr", _img.SINGLE_DOUBLE_PIPE_CONNECTOR_SVG.icon()), ("DPCnr", _img.DOUBLE_DOUBLE_PIPE_CONNECTOR_SVG.icon()), ("TVentil", _img.T_VENTIL_SVG.icon()), ("WTap_main", _img.W_TAP_MAIN_SVG.icon()), ("WTap", _img.W_TAP_SVG.icon()), ("Pump", _img.PUMP_SVG.icon()), ("Collector", _img.COLLECTOR_SVG.icon()), ("GroundSourceHx", _img.GROUND_SOURCE_HX_SVG.icon()), ("PV", _img.PV_SVG.icon()), ("HP", _img.HP_SVG.icon()), ("HPTwoHx", _img.HP_TWO_HX_SVG.icon()), ("HPDoubleDual", _img.HP_DOUBLE_DUAL_SVG.icon()), ("HPDual", _img.HP_DUAL_SVG.icon()), ("AirSourceHP", _img.AIR_SOURCE_HP_SVG.icon()), ("StorageTank", _img.STORAGE_TANK_SVG.icon()), ("IceStorage", _img.ICE_STORAGE_SVG.icon()), ("PitStorage", _img.PIT_STORAGE_SVG.icon()), ("IceStorageTwoHx", _img.ICE_STORAGE_TWO_HX_SVG.icon()), ("ExternalHx", _img.EXTERNAL_HX_SVG.icon()), ("Radiator", _img.RADIATOR_SVG.icon()), ("Boiler", _img.BOILER_SVG.icon()), ("Sink", _img.SINK_SVG.icon()), ("Source", _img.SOURCE_SVG.icon()), ("SourceSink", _img.SOURCE_SINK_SVG.icon()), ("Geotherm", _img.GEOTHERM_SVG.icon()), ("Water", _img.WATER_SVG.icon()), ("Crystalizer", _img.CRYSTALIZER_SVG.icon()), ("GenericBlock", _img.GENERIC_BLOCK_PNG.icon()), ("GraphicalItem", _img.GENERIC_ITEM_PNG.icon()), ] libItems = [ QtGui.QStandardItem(icon, name) for name, icon in componentNamesWithIcon ] for i in libItems: self.libraryModel.appendRow(i) self.libraryBrowserView.setModel(self.libraryModel) self.libraryBrowserView.setViewMode(self.libraryBrowserView.IconMode) self.libraryBrowserView.setDragDropMode( self.libraryBrowserView.DragOnly) self.diagramScene = Scene(self) self.diagramView = View(self.diagramScene, self) # For list view self.vertL = QVBoxLayout() self.vertL.addWidget(self.libraryBrowserView) self.vertL.setStretchFactor(self.libraryBrowserView, 2) self.listV = QListWidget() self.vertL.addWidget(self.listV) self.vertL.setStretchFactor(self.listV, 1) # for file browser self.projectPath = "" self.fileList = [] if loadValue == "new" or loadValue == "json": self.createProjectFolder() self.fileBrowserLayout = QVBoxLayout() self.pathLayout = QHBoxLayout() self.projectPathLabel = QLabel("Project Path:") self.PPL = QLineEdit(self.projectFolder) self.PPL.setDisabled(True) self.pathLayout.addWidget(self.projectPathLabel) self.pathLayout.addWidget(self.PPL) self.scroll = QScrollArea() self.scroll.setWidgetResizable(True) self.splitter = QSplitter(Qt.Vertical, ) self.splitter.setChildrenCollapsible(False) self.scroll.setWidget(self.splitter) self.scroll.setFixedWidth(350) self.fileBrowserLayout.addLayout(self.pathLayout) self.fileBrowserLayout.addWidget(self.scroll) self.createDdckTree(self.projectFolder) if loadValue == "new" or loadValue == "json": self.createConfigBrowser(self.projectFolder) self.copyGenericFolder(self.projectFolder) self.createHydraulicDir(self.projectFolder) self.createWeatherAndControlDirs(self.projectFolder) self.horizontalLayout.addLayout(self.vertL) self.horizontalLayout.addWidget(self.diagramView) self.horizontalLayout.addLayout(self.fileBrowserLayout) self.horizontalLayout.setStretchFactor(self.diagramView, 5) self.horizontalLayout.setStretchFactor(self.libraryBrowserView, 1) self._currentlyDraggedConnectionFromPort = None self.connectionList = [] self.trnsysObj = [] self.graphicalObj = [] self.fluids = _hlm.Fluids([]) self.hydraulicLoops = _hlm.HydraulicLoops([]) self.copyGroupList = QGraphicsItemGroup() self.selectionGroupList = QGraphicsItemGroup() self.printerUnitnr = 0 # Different colors for connLineColor colorsc = "red" linePx = 4 if colorsc == "red": connLinecolor = QColor(Qt.red) elif colorsc == "blueish": connLinecolor = QColor(3, 124, 193) # Blue elif colorsc == "darkgray": connLinecolor = QColor(140, 140, 140) # Gray else: connLinecolor = QColor(196, 196, 196) # Gray # Only for displaying on-going creation of connection self.connLine = QLineF() self.connLineItem = QGraphicsLineItem(self.connLine) self.connLineItem.setPen(QtGui.QPen(connLinecolor, linePx)) self.connLineItem.setVisible(False) self.diagramScene.addItem(self.connLineItem) # For line that shows quickly up when using the align mode self.alignYLine = QLineF() self.alignYLineItem = QGraphicsLineItem(self.alignYLine) self.alignYLineItem.setPen(QtGui.QPen(QColor(196, 249, 252), 2)) self.alignYLineItem.setVisible(False) self.diagramScene.addItem(self.alignYLineItem) # For line that shows quickly up when using align mode self.alignXLine = QLineF() self.alignXLineItem = QGraphicsLineItem(self.alignXLine) self.alignXLineItem.setPen(QtGui.QPen(QColor(196, 249, 252), 2)) self.alignXLineItem.setVisible(False) self.diagramScene.addItem(self.alignXLineItem) if loadValue == "load" or loadValue == "copy": self._decodeDiagram(os.path.join(self.projectFolder, self.diagramName), loadValue=loadValue) elif loadValue == "json": self._decodeDiagram(jsonPath, loadValue=loadValue) # Debug function def dumpInformation(self): self.logger.debug("Diagram information:") self.logger.debug("Mode is " + str(self.editorMode)) self.logger.debug("Next ID is " + str(self.idGen.getID())) self.logger.debug("Next cID is " + str(self.idGen.getConnID())) self.logger.debug("TrnsysObjects are:") for t in self.trnsysObj: self.logger.debug(str(t)) self.logger.debug("") self.logger.debug("Scene items are:") sItems = self.diagramScene.items() for it in sItems: self.logger.info(str(it)) self.logger.debug("") for c in self.connectionList: c.printConn() self.logger.debug("") # Connections related methods def startConnection(self, port): self._currentlyDraggedConnectionFromPort = port def _createConnection(self, startPort, endPort) -> None: if startPort is not endPort: if (isinstance(startPort.parent, StorageTank) and isinstance(endPort.parent, StorageTank) and startPort.parent != endPort.parent): msgSTank = QMessageBox(self) msgSTank.setText( "Storage Tank to Storage Tank connection is not working atm!" ) msgSTank.exec_() isValidSinglePipeConnection = isinstance( startPort, SinglePipePortItem) and isinstance( endPort, SinglePipePortItem) if isValidSinglePipeConnection: command = CreateSinglePipeConnectionCommand( startPort, endPort, self) elif isinstance(startPort, DoublePipePortItem) and isinstance( endPort, DoublePipePortItem): command = CreateDoublePipeConnectionCommand( startPort, endPort, self) else: raise AssertionError( "Can only connect port items. Also, they have to be of the same type." ) self.parent().undoStack.push(command) def sceneMouseMoveEvent(self, event): """ This function is for dragging and connecting one port to another. When dragging, the fromPort will remain enlarged and black in color and when the toPort is hovered over, it will be enlarged and turn red. A port's details will also be displayed at the widget when they are hovered over. """ fromPort = self._currentlyDraggedConnectionFromPort if not fromPort: return fromX = fromPort.scenePos().x() fromY = fromPort.scenePos().y() toX = event.scenePos().x() toY = event.scenePos().y() self.connLine.setLine(fromX, fromY, toX, toY) self.connLineItem.setLine(self.connLine) self.connLineItem.setVisible(True) hitPortItem = self._getHitPortItemOrNone(event) if not hitPortItem: return mousePosition = event.scenePos() portItemX = hitPortItem.scenePos().x() portItemY = hitPortItem.scenePos().y() distance = _math.sqrt((mousePosition.x() - portItemX)**2 + (mousePosition.y() - portItemY)**2) if distance <= 3.5: hitPortItem.enlargePortSize() hitPortItem.innerCircle.setBrush(hitPortItem.ashColorR) self.listV.clear() hitPortItem.debugprint() else: hitPortItem.resetPortSize() hitPortItem.innerCircle.setBrush(hitPortItem.visibleColor) self.listV.clear() fromPort.debugprint() fromPort.enlargePortSize() fromPort.innerCircle.setBrush(hitPortItem.visibleColor) def _getHitPortItemOrNone(self, event: QEvent) -> _tp.Optional[PortItemBase]: fromPort = self._currentlyDraggedConnectionFromPort mousePosition = event.scenePos() relevantPortItems = self._getRelevantHitPortItems( mousePosition, fromPort) if not relevantPortItems: return None numberOfHitPortsItems = len(relevantPortItems) if numberOfHitPortsItems > 1: raise NotImplementedError( "Can't deal with overlapping port items.") hitPortItem = relevantPortItems[0] return hitPortItem def sceneMouseReleaseEvent(self, event): if not self._currentlyDraggedConnectionFromPort: return fromPort = self._currentlyDraggedConnectionFromPort self._currentlyDraggedConnectionFromPort = None self.connLineItem.setVisible(False) mousePosition = event.scenePos() relevantPortItems = self._getRelevantHitPortItems( mousePosition, fromPort) numberOfHitPortsItems = len(relevantPortItems) if numberOfHitPortsItems > 1: raise NotImplementedError( "Can't deal with overlapping port items.") if numberOfHitPortsItems == 1: toPort = relevantPortItems[0] if toPort != fromPort: self._createConnection(fromPort, toPort) def _getRelevantHitPortItems( self, mousePosition: QPointF, fromPort: PortItemBase) -> _tp.Sequence[PortItemBase]: hitItems = self.diagramScene.items(mousePosition) relevantPortItems = [ i for i in hitItems if isinstance(i, PortItemBase) and type(i) == type(fromPort) and not i.connectionList ] return relevantPortItems def cleanUpConnections(self): for c in self.connectionList: c.niceConn() def exportHydraulics(self, exportTo=_tp.Literal["ddck", "mfs"]): assert exportTo in ["ddck", "mfs"] if not self._isHydraulicConnected(): messageBox = QMessageBox() messageBox.setWindowTitle("Hydraulic not connected") messageBox.setText( "You need to connect all port items before you can export the hydraulics." ) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.exec() return self.logger.info( "------------------------> START OF EXPORT <------------------------" ) self.sortTrnsysObj() fullExportText = "" ddckFolder = os.path.join(self.projectFolder, "ddck") if exportTo == "mfs": mfsFileName = self.diagramName.split(".")[0] + "_mfs.dck" exportPath = os.path.join(self.projectFolder, mfsFileName) elif exportTo == "ddck": exportPath = os.path.join(ddckFolder, "hydraulic\\hydraulic.ddck") if self._doesFileExistAndDontOverwrite(exportPath): return None self.logger.info("Printing the TRNSYS file...") if exportTo == "mfs": header = open(os.path.join(ddckFolder, "generic\\head.ddck"), "r") headerLines = header.readlines() for line in headerLines: if line[:4] == "STOP": fullExportText += "STOP = 1 \n" else: fullExportText += line header.close() elif exportTo == "ddck": fullExportText += "*************************************\n" fullExportText += "** BEGIN hydraulic.ddck\n" fullExportText += "*************************************\n\n" fullExportText += "*************************************\n" fullExportText += "** Outputs to energy balance in kWh\n" fullExportText += ( "** Following this naming standard : qSysIn_name, qSysOut_name, elSysIn_name, elSysOut_name\n" ) fullExportText += "*************************************\n" fullExportText += "EQUATIONS 1\n" fullExportText += "qSysOut_PipeLoss = PipeLossTot\n" simulationUnit = 450 simulationType = 935 descConnLength = 20 exporter = self._createExporter() blackBoxProblem, blackBoxText = exporter.exportBlackBox( exportTo=exportTo) if blackBoxProblem: return None fullExportText += blackBoxText if exportTo == "mfs": fullExportText += exporter.exportMassFlows() fullExportText += exporter.exportPumpOutlets() fullExportText += exporter.exportDivSetting(simulationUnit - 10) fullExportText += exporter.exportDoublePipeParameters( exportTo=exportTo) fullExportText += exporter.exportParametersFlowSolver( simulationUnit, simulationType, descConnLength) fullExportText += exporter.exportInputsFlowSolver() fullExportText += exporter.exportOutputsFlowSolver(simulationUnit) fullExportText += exporter.exportFluids() + "\n" fullExportText += exporter.exportHydraulicLoops() + "\n" fullExportText += exporter.exportPipeAndTeeTypesForTemp( simulationUnit + 1) # DC-ERROR fullExportText += exporter.exportPrintPipeLosses() fullExportText += exporter.exportMassFlowPrinter( self.printerUnitnr, 15) fullExportText += exporter.exportTempPrinter(self.printerUnitnr + 1, 15) if exportTo == "mfs": fullExportText += "CONSTANTS 1\nTRoomStore=1\n" fullExportText += "ENDS" self.logger.info( "------------------------> END OF EXPORT <------------------------" ) if exportTo == "mfs": f = open(exportPath, "w") f.truncate(0) f.write(fullExportText) f.close() elif exportTo == "ddck": if fullExportText[:1] == "\n": fullExportText = fullExportText[1:] hydraulicFolder = os.path.split(exportPath)[0] if not (os.path.isdir(hydraulicFolder)): os.makedirs(hydraulicFolder) f = open(exportPath, "w") f.truncate(0) f.write(fullExportText) f.close() try: lines = _du.loadDeck(exportPath, eraseBeginComment=True, eliminateComments=True) _du.checkEquationsAndConstants(lines, exportPath) except Exception as error: errorMessage = f"An error occurred while exporting the system hydraulics: {error}" _errs.showErrorMessageBox(errorMessage) return None return exportPath def _createExporter(self) -> Export: massFlowContributors = self._getMassFlowContributors() exporter = Export(massFlowContributors, self) return exporter def _getMassFlowContributors( self) -> _tp.Sequence[_mfs.MassFlowNetworkContributorMixin]: massFlowContributors = [ o for o in self.trnsysObj if isinstance(o, _mfs.MassFlowNetworkContributorMixin) ] return massFlowContributors def _isHydraulicConnected(self) -> bool: for obj in self.trnsysObj: if not isinstance(obj, _mfs.MassFlowNetworkContributorMixin): continue internalPiping = obj.getInternalPiping() for portItem in internalPiping.modelPortItemsToGraphicalPortItem.values( ): if not portItem.connectionList: return False return True def _doesFileExistAndDontOverwrite(self, folderPath): if not _pl.Path(folderPath).exists(): return False qmb = QMessageBox(self) qmb.setText( f"Warning: {folderPath} already exists. Do you want to overwrite it or cancel?" ) qmb.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) qmb.setDefaultButton(QMessageBox.Cancel) ret = qmb.exec() if ret == QMessageBox.Cancel: self.canceled = True self.logger.info("Canceling") return True self.canceled = False self.logger.info("Overwriting") return False def exportHydraulicControl(self): self.logger.info( "------------------------> START OF EXPORT <------------------------" ) self.sortTrnsysObj() fullExportText = "" ddckFolder = os.path.join(self.projectFolder, "ddck") hydCtrlPath = os.path.join(ddckFolder, "control\\hydraulic_control.ddck") if _pl.Path(hydCtrlPath).exists(): qmb = QMessageBox(self) qmb.setText( "Warning: " + "The file hydraulic_control.ddck already exists in the control folder. Do you want to overwrite it or cancel?" ) qmb.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) qmb.setDefaultButton(QMessageBox.Cancel) ret = qmb.exec() if ret == QMessageBox.Save: self.canceled = False self.logger.info("Overwriting") else: self.canceled = True self.logger.info("Canceling") return fullExportText += "*************************************\n" fullExportText += "**BEGIN hydraulic_control.ddck\n" fullExportText += "*************************************\n" simulationUnit = 450 exporter = self._createExporter() fullExportText += exporter.exportPumpOutlets() fullExportText += exporter.exportMassFlows() fullExportText += exporter.exportDivSetting(simulationUnit - 10) self.logger.info( "------------------------> END OF EXPORT <------------------------" ) if fullExportText[:1] == "\n": fullExportText = fullExportText[1:] controlFolder = os.path.split(hydCtrlPath)[0] if not (os.path.isdir(controlFolder)): os.makedirs(controlFolder) f = open(str(hydCtrlPath), "w") f.truncate(0) f.write(fullExportText) f.close() return hydCtrlPath def sortTrnsysObj(self): res = self.trnsysObj.sort(key=self.sortId) for s in self.trnsysObj: self.logger.debug("s has tr id " + str(s.trnsysId) + " has dname " + s.displayName) def sortId(self, l1): """ Sort function returning a sortable key Parameters ---------- l1 : Block/Connection Returns ------- """ return l1.trnsysId def setName(self, newName): self.diagramName = newName def delBlocks(self): """ Deletes the whole diagram Returns ------- """ self.hydraulicLoops.clear() while len(self.trnsysObj) > 0: self.logger.info("In deleting...") self.trnsysObj[0].deleteBlock() while len(self.graphicalObj) > 0: self.graphicalObj[0].deleteBlock() # Encoding / decoding def encodeDiagram(self, filename): """ Encodes the diagram to a json file. Parameters ---------- filename : str Returns ------- """ self.logger.info("filename is at encoder " + str(filename)) # if filename != "": with open(filename, "w") as jsonfile: json.dump(self, jsonfile, indent=4, sort_keys=True, cls=Encoder) def _decodeDiagram(self, filename, loadValue="load"): self.logger.info("Decoding " + filename) with open(filename, "r") as jsonfile: blocklist = json.load(jsonfile, cls=Decoder, editor=self) blockFolderNames = [] for j in blocklist["Blocks"]: for k in j: if isinstance(k, BlockItem): k.setParent(self.diagramView) k.changeSize() self.diagramScene.addItem(k) blockFolderNames.append(k.displayName) if isinstance(k, StorageTank): k.updateImage() if isinstance(k, GraphicalItem): k.setParent(self.diagramView) self.diagramScene.addItem(k) if isinstance(k, dict): if "__idDct__" in k: # here we don't set the ids because the copyGroup would need access to idGen self.logger.debug( "Found the id dict while loading, not setting the ids" ) self.idGen.setID(k["GlobalId"]) self.idGen.setTrnsysID(k["trnsysID"]) self.idGen.setConnID(k["globalConnID"]) if "__nameDct__" in k: self.logger.debug("Found the name dict while loading") if loadValue == "load": self.diagramName = k["DiagramName"] blockFolderNames.append("generic") blockFolderNames.append("hydraulic") blockFolderNames.append("weather") blockFolderNames.append("control") ddckFolder = os.path.join(self.projectFolder, "ddck") ddckFolders = os.listdir(ddckFolder) additionalFolders = [] for folder in ddckFolders: if folder not in blockFolderNames and "StorageTank" not in folder: additionalFolders.append(folder) if len(additionalFolders) > 0: warnBox = QMessageBox() warnBox.setWindowTitle("Additional ddck-folders") if len(additionalFolders) == 1: text = "The following ddck-folder does not have a corresponding component in the diagram:" else: text = "The following ddck-folders do not have a corresponding component in the diagram:" for folder in additionalFolders: text += "\n\t" + folder warnBox.setText(text) warnBox.setStandardButtons(QMessageBox.Ok) warnBox.setDefaultButton(QMessageBox.Ok) warnBox.exec() for t in self.trnsysObj: t.assignIDsToUninitializedValuesAfterJsonFormatMigration( self.idGen) self.logger.debug("Tr obj is" + str(t) + " " + str(t.trnsysId)) if hasattr(t, "isTempering"): self.logger.debug("tv has " + str(t.isTempering)) self._decodeHydraulicLoops(blocklist) def _decodeHydraulicLoops(self, blocklist): singlePipeConnections = [ c for c in self.connectionList if isinstance(c, SinglePipeConnection) ] if "hydraulicLoops" not in blocklist: hydraulicLoops = _hlmig.createLoops(singlePipeConnections, self.fluids.WATER) else: serializedHydraulicLoops = blocklist["hydraulicLoops"] hydraulicLoops = _hlm.HydraulicLoops.createFromJson( serializedHydraulicLoops, singlePipeConnections, self.fluids) self.hydraulicLoops = hydraulicLoops def exportSvg(self): """ For exporting a svg file (text is still too large) Returns ------- """ generator = QSvgGenerator() generator.setResolution(300) generator.setSize( QSize(self.diagramScene.width(), self.diagramScene.height())) # generator.setViewBox(QRect(0, 0, 800, 800)) generator.setViewBox(self.diagramScene.sceneRect()) generator.setFileName("VectorGraphicsExport.svg") painter = QPainter() painter.begin(generator) painter.setRenderHint(QPainter.Antialiasing) self.diagramScene.render(painter) painter.end() # Saving related def save(self, showWarning=True): """ If saveas has not been used, diagram will be saved in "/diagrams" If saveas has been used, diagram will be saved in self.saveAsPath Returns ------- """ self.diagramName = os.path.split(self.projectFolder)[-1] + ".json" diagramPath = os.path.join(self.projectFolder, self.diagramName) if os.path.isfile(diagramPath) and showWarning: qmb = QMessageBox(self) qmb.setText( "Warning: " + "This diagram name exists already. Do you want to overwrite or cancel?" ) qmb.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) qmb.setDefaultButton(QMessageBox.Cancel) ret = qmb.exec() if ret != QMessageBox.Save: self.logger.info("Canceling") return self.logger.info("Overwriting") self.encodeDiagram(diagramPath) self.encodeDiagram(diagramPath) msgb = QMessageBox(self) msgb.setText("Saved diagram at " + diagramPath) msgb.exec() def saveToProject(self): projectPath = self.projectPath def renameDiagram(self, newName): """ Parameters ---------- newName Returns ------- """ if self.saveAsPath.name != "": # print("Path name is " + self.saveAsPath.name) if newName + ".json" in self.saveAsPath.glob("*"): QMessageBox( self, "Warning", "This diagram name exists already in the directory." " Please rename this diagram") else: self.saveAsPath = _pl.Path( self.saveAsPath.stem[0:self.saveAsPath.name. index(self.diagramName)] + newName) self.diagramName = newName self.parent().currentFile = newName # fromPath = self.projectFolder # destPath = os.path.dirname(__file__) # destPath = os.path.join(destPath, 'default') # destPath = os.path.join(destPath, newName) # os.rename(fromPath, destPath) # print("Path is now: " + str(self.saveAsPath)) # print("Diagram name is: " + self.diagramName) def saveAtClose(self): self.logger.info("saveaspath is " + str(self.saveAsPath)) # closeDialog = closeDlg() # if closeDialog.closeBool: filepath = _pl.Path( _pl.Path(__file__).resolve().parent.joinpath("recent")) self.encodeDiagram(str(filepath.joinpath(self.diagramName + ".json"))) # Mode related def setAlignMode(self, b): self.alignMode = True def setEditorMode(self, b): self.editorMode = b def setMoveDirectPorts(self, b): """ Sets the bool moveDirectPorts. When mouse released in diagramScene, moveDirectPorts is set to False again Parameters ---------- b : bool Returns ------- """ self.moveDirectPorts = b def setSnapGrid(self, b): self.snapGrid = b def setSnapSize(self, s): self.snapSize = s def setConnLabelVis(self, isVisible: bool) -> None: for c in self.trnsysObj: if isinstance(c, ConnectionBase): c.setLabelVisible(isVisible) if isinstance(c, BlockItem): c.label.setVisible(isVisible) if isinstance(c, TVentil): c.posLabel.setVisible(isVisible) def updateConnGrads(self): for t in self.trnsysObj: if isinstance(t, ConnectionBase): t.updateSegGrads() # Dialog calls def showBlockDlg(self, bl): c = BlockDlg(bl, self) def showDoublePipeBlockDlg(self, bl): c = DoublePipeBlockDlg(bl, self) def showPumpDlg(self, bl): c = PumpDlg(bl, self) def showDiagramDlg(self): c = diagramDlg(self) def showGenericPortPairDlg(self, bl): c = GenericPortPairDlg(bl, self) def showHxDlg(self, hx): c = hxDlg(hx, self) def showSegmentDlg(self, seg): c = segmentDlg(seg, self) def showTVentilDlg(self, bl): c = TVentilDlg(bl, self) def showConfigStorageDlg(self, bl): c = ConfigureStorageDialog(bl, self) def getConnection(self, n): return self.connectionList[int(n)] # Unused def create_icon(self, map_icon): map_icon.fill() painter = QPainter(map_icon) painter.fillRect(10, 10, 40, 40, QColor(88, 233, 252)) # painter.setBrush(Qt.red) painter.setBrush(QColor(252, 136, 98)) painter.drawEllipse(36, 2, 15, 15) painter.setBrush(Qt.yellow) painter.drawEllipse(20, 20, 20, 20) painter.end() def setTrnsysIdBack(self): self.idGen.trnsysID = max(t.trnsysId for t in self.trnsysObj) def findStorageCorrespPorts1(self, portList): """ This function gets the ports on the other side of pipes connected to a port of the StorageTank. Unused Parameters ---------- portList : :obj:`List` of :obj:`PortItems` Returns ------- """ res = [] # print("Finding c ports") for p in portList: if len(p.connectionList) > 0: # check if not >1 needed # connectionList[0] is the hidden connection created when the portPair is i = 0 # while type(p.connectionList[i].fromPort.parent) is StorageTank and type(p.connectionList[i].toPort.parent) is StorageTank: while (p.connectionList[i].fromPort.parent) == ( p.connectionList[i].toPort.parent): i += 1 if len(p.connectionList) >= i + 1: if p.connectionList[i].fromPort is p: res.append(p.connectionList[i].toPort) elif p.connectionList[i].toPort is p: res.append(p.connectionList[i].fromPort) else: self.logger.debug("Port is not fromPort nor toPort") # [print(p.parent.displayName) for p in res] return res def printPDF(self): """ --------------------------------------------- Export diagram as pdf onto specified folder fn = user input directory --------------------------------------------- """ fn, _ = QFileDialog.getSaveFileName(self, "Export PDF", None, "PDF files (.pdf);;All Files()") if fn != "": if QFileInfo(fn).suffix() == "": fn += ".pdf" printer = QPrinter(QPrinter.HighResolution) printer.setOrientation(QPrinter.Landscape) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fn) painter = QPainter(printer) self.diagramScene.render(painter) painter.end() self.logger.info("File exported to %s" % fn) def openProject(self): self.projectPath = str( QFileDialog.getExistingDirectory(self, "Select Project Path")) if self.projectPath != "": test = self.parent() self.parent().newDia() self.PPL.setText(self.projectPath) loadPath = os.path.join(self.projectPath, "ddck") self.createConfigBrowser(self.projectPath) self.copyGenericFolder(self.projectPath) self.createHydraulicDir(self.projectPath) self.createWeatherAndControlDirs(self.projectPath) self.createDdckTree(loadPath) # todo : open diagram # todo : add files into list def createDdckTree(self, loadPath): treeToRemove = self.findChild(QTreeView, "ddck") try: # treeToRemove.hide() treeToRemove.deleteLater() except AttributeError: self.logger.debug("Widget doesnt exist!") else: self.logger.debug("Deleted widget") if self.projectPath == "": loadPath = os.path.join(loadPath, "ddck") if not os.path.exists(loadPath): os.makedirs(loadPath) self.model = MyQFileSystemModel() self.model.setRootPath(loadPath) self.model.setName("ddck") self.tree = MyQTreeView(self.model, self) self.tree.setModel(self.model) self.tree.setRootIndex(self.model.index(loadPath)) self.tree.setObjectName("ddck") self.tree.setMinimumHeight(600) self.tree.setSortingEnabled(True) self.splitter.insertWidget(0, self.tree) def createConfigBrowser(self, loadPath): self.layoutToRemove = self.findChild(QHBoxLayout, "Config_Layout") try: # treeToRemove.hide() self.layoutToRemove.deleteLater() except AttributeError: self.logger.debug("Widget doesnt exist!") else: self.logger.debug("Deleted widget") runConfigData = self._getPackageResourceData("templates/run.config") runConfigPath = _pl.Path(loadPath) / "run.config" runConfigPath.write_bytes(runConfigData) self.HBox = QHBoxLayout() self.refreshButton = QPushButton(self) self.refreshButton.setIcon(_img.ROTATE_TO_RIGHT_PNG.icon()) self.refreshButton.clicked.connect(self.refreshConfig) self.model = MyQFileSystemModel() self.model.setRootPath(loadPath) self.model.setName("Config File") self.model.setFilter(QDir.Files) self.tree = MyQTreeView(self.model, self) self.tree.setModel(self.model) self.tree.setRootIndex(self.model.index(loadPath)) self.tree.setObjectName("config") self.tree.setFixedHeight(60) self.tree.setSortingEnabled(False) self.HBox.addWidget(self.refreshButton) self.HBox.addWidget(self.tree) self.HBox.setObjectName("Config_Layout") self.fileBrowserLayout.addLayout(self.HBox) def createProjectFolder(self): if not os.path.exists(self.projectFolder): os.makedirs(self.projectFolder) def refreshConfig(self): # configPath = os.path.dirname(__file__) # configPath = os.path.join(configPath, 'project') # configPath = os.path.join(configPath, self.date_time) # emptyConfig = os.path.join(configPath, 'run.config') if self.projectPath == "": localPath = self.projectFolder else: localPath = self.projectPath self.configToEdit = os.path.join(localPath, "run.config") os.remove(self.configToEdit) shutil.copy(self.emptyConfig, localPath) self.configToEdit = os.path.join(localPath, "run.config") localDdckPath = os.path.join(localPath, "ddck") with open(self.configToEdit, "r") as file: lines = file.readlines() localPathStr = "string LOCAL$ %s" % str(localDdckPath) # localPathStr.replace('/', '\\') lines[21] = localPathStr + "\n" with open(self.configToEdit, "w") as file: file.writelines(lines) # print(localPathStr) self.userInputList() def userInputList(self): self.logger.debug(self.fileList) dia = FileOrderingDialog(self.fileList, self) def copyGenericFolder(self, loadPath): genericFolderPath = _pl.Path(loadPath) / "ddck" / "generic" if not genericFolderPath.exists(): self.logger.info("Creating %s", genericFolderPath) genericFolderPath.mkdir() headData = self._getPackageResourceData("templates/generic/head.ddck") self.logger.info("Copying head.ddck") (genericFolderPath / "head.ddck").write_bytes(headData) endData = self._getPackageResourceData("templates/generic/end.ddck") self.logger.info("Copying end.ddck") (genericFolderPath / "end.ddck").write_bytes(endData) @staticmethod def _getPackageResourceData(resourcePath): data = _pu.get_data(_tgui.__name__, resourcePath) assert data, f"{resourcePath} package resource not found" return data def createHydraulicDir(self, projectPath): self.hydraulicFolder = os.path.join(projectPath, "ddck") self.hydraulicFolder = os.path.join(self.hydraulicFolder, "hydraulic") if not os.path.exists(self.hydraulicFolder): self.logger.info("Creating " + self.hydraulicFolder) os.makedirs(self.hydraulicFolder) def createWeatherAndControlDirs(self, projectPath): ddckFolder = os.path.join(projectPath, "ddck") weatherFolder = os.path.join(ddckFolder, "weather") controlFolder = os.path.join(ddckFolder, "control") if not os.path.exists(weatherFolder): self.logger.info("Creating " + weatherFolder) os.makedirs(weatherFolder) if not os.path.exists(controlFolder): self.logger.info("Creating " + controlFolder) os.makedirs(controlFolder) def editHydraulicLoop(self, singlePipeConnection: SinglePipeConnection): assert isinstance(singlePipeConnection.fromPort, SinglePipePortItem) hydraulicLoop = self.hydraulicLoops.getLoopForExistingConnection( singlePipeConnection) _hledit.edit(hydraulicLoop, self.hydraulicLoops, self.fluids)
class OTMainWindow(QWidget): def __init__(self, parent=None): super(OTMainWindow, self).__init__(parent, Qt.Window) self.setWindowTitle('OPC Python Tester') self.layout = QVBoxLayout() # self.tree = QTreeWidget(self) self.tree.setHeaderLabel('OPC server tree') self.tree_root = QTreeWidgetItem() self.tree_root.setText(0, 'not connected') self.tree.addTopLevelItem(self.tree_root) self.tree.itemDoubleClicked.connect(self.on_tree_item_double_clicked) # self.table = QTableWidget(self) self.table.setRowCount(0) self.table_column_labels = [ 'item_id', 'value', 'type', 'access', 'quality', 'timestamp'] self.table.setColumnCount(len(self.table_column_labels)) self.table.setHorizontalHeaderLabels(self.table_column_labels) self.table.horizontalHeader().setStretchLastSection(True) # self.splitter = QSplitter(Qt.Horizontal, self) self.splitter.setChildrenCollapsible(False) self.splitter.setHandleWidth(10) self.layout.addWidget(self.splitter) # final self.splitter.addWidget(self.tree) self.splitter.addWidget(self.table) self.splitter.setSizes([150, 300]) self.setLayout(self.layout) # self.opcsrv = None self.cur_server_info = {} self.cur_comp_name = '' self.watched_itemids = [] self.ssdialog = ServerSelectDialog(self) ssel_ret = self.ssdialog.exec_() if ssel_ret == QDialog.Accepted: self.do_connect(self.ssdialog.selected_server, self.ssdialog.selected_comp_name) else: print('Connection cancelled') self.timer = QTimer(self) self.timer.timeout.connect(self.on_timer_timeout) self.timer.start(1000) # every 1 second def do_connect(self, srv_info: dict, comp_name: str): print('Connecting to "{0}" ({1}) on comp: {2}...'.format( srv_info['desc'], srv_info['guid'], comp_name)) self.opcsrv = opc_helper.opc_connect(srv_info['guid'], comp_name) if self.opcsrv is None: return self.cur_comp_name = comp_name self.cur_server_info = srv_info print(self.opcsrv.get_status()) self.fill_tree() def fill_tree(self): self.tree.clear() if self.opcsrv is None: return self.tree_root = QTreeWidgetItem(self.tree) self.tree_root.setChildIndicatorPolicy(QTreeWidgetItem.DontShowIndicatorWhenChildless) root_title = '{0}'.format(self.cur_server_info['desc']) if self.cur_comp_name != '': root_title = '{0} ({1})'.format(self.cur_server_info['desc'], self.cur_comp_name) self.tree_root.setText(0, root_title) self.tree.addTopLevelItem(self.tree_root) server_tree = self.opcsrv.browse(flat=False) # for oitem in server_tree: self.fill_item(oitem, self.tree_root) def fill_item(self, item: dict, parent: QTreeWidgetItem): tree_item = QTreeWidgetItem() tree_item.setChildIndicatorPolicy(QTreeWidgetItem.DontShowIndicatorWhenChildless) tree_item.setText(0, item['name']) if item['children'] is None: # set userdata = item_id only if this IS a LEAF node tree_item.setData(0, Qt.UserRole, item['item_id']) # column, role, data parent.addChild(tree_item) # recurse into children if item['children'] is not None: for oitem in item['children']: self.fill_item(oitem, tree_item) @pyqtSlot(QTreeWidgetItem, int) def on_tree_item_double_clicked(self, item: QTreeWidgetItem, column: int): # void itemDoubleClicked(QTreeWidgetItem * item, int column) # virtual QVariant data(int column, int role) const item_data = item.data(0, Qt.UserRole) if item_data is None: return item_id = str(item_data) print('Double click on [{0}]'.format(item_id)) self.opcsrv.get_item(item_id) if item_id not in self.watched_itemids: self.watched_itemids.append(item_id) @pyqtSlot() def on_timer_timeout(self): num_items = len(self.watched_itemids) self.table.setRowCount(num_items) i = 0 while i < num_items: item_id = self.watched_itemids[i] item_value = self.opcsrv.get_item(item_id) item_info = self.opcsrv.get_item_info(item_id) # twi = QTableWidgetItem(str(item_id)) self.table.setItem(i, 0, twi) # twi = QTableWidgetItem(str(item_value)) self.table.setItem(i, 1, twi) # twi = QTableWidgetItem(str(item_info['type'])) self.table.setItem(i, 2, twi) # twi = QTableWidgetItem(str(item_info['access_rights'])) self.table.setItem(i, 3, twi) # twi = QTableWidgetItem(str(item_info['quality'])) self.table.setItem(i, 4, twi) # ts_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item_info['timestamp'])) twi = QTableWidgetItem(str(ts_str)) self.table.setItem(i, 5, twi) # i += 1