class DynaFrame(QFrame): """ The DynaFrame class wraps one custom widget which can be replaced with another during runtime. """ def __init__(self, parent, widget: QWidget=None): super(__class__, self).__init__(parent) layout = QHBoxLayout() self._stk_widget = QStackedWidget() if widget is not None: self.setWidget(widget) layout.addWidget(self._stk_widget) self.setLayout(layout) def setWidget(self, widget: QWidget): """ Replaces the currently present widget (if any) with the specified one. """ self._clear_stack_widget() self._stk_widget.addWidget(widget) def getWidget(self) -> QWidget: """ Returns the widget wrapped in the frame. """ return self._stk_widget.widget(0) def _clear_stack_widget(self): """ Clears all widgets which were added to the stack. """ count = self._stk_widget.count() assert 0 <= count <= 1 for index in range(0, count): widget = self._stk_widget.widget(index) self._stk_widget.removeWidget(widget)
class MainWindow(QMainWindow): def __init__(self): super().__init__() self._widgetList = [SearchWidget, TitleViewer, ImageViewer] self.__createWidgets() def __createWidgets(self): self._stack = QStackedWidget() # self.setWindowTitle("Cute Manga") self._stack.addWidget(SearchWidget(parent=self, backEnd=mangahere)) # self.setCentralWidget(self._stack) def goBack(self) -> NoReturn: ci = self._stack.currentIndex() if ci == 0: sys.exit(0) else: cw = self._stack.currentWidget() self._stack.setCurrentIndex(ci - 1) self._stack.removeWidget(cw) def goTo(self, widget: QWidget) -> NoReturn: ci = self._stack.currentIndex() self._stack.addWidget(widget) self._stack.setCurrentIndex(ci + 1)
class WorkspaceStatus(BaseWorkspace): def setupWorkspace(self): self.pile = QStackedWidget(self) self.wLayout.addWidget(self.pile) self.dlgs = weakref.WeakValueDictionary() self.pile.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding) self.push(DefaultStatusWidget(), 'default') def push(self, widget, name): idx = self.pile.addWidget(widget) self.pile.setCurrentIndex(idx) self.dlgs[name] = widget return idx, widget def clear(self, name): if name in self.dlgs: w = self.dlgs[name] idx = self.pile.indexOf(w) if idx: self.pile.removeWidget(w) def pushProgress(self, name): return self.push(DefaultProgressDialog(), name) async def pushRunDialog(self, dialog, name): dialog.setEnabled(True) idx, w = self.push(dialog, name) await runDialogAsync(dialog)
class GOQT(QWidget): def __init__(self, maingo): super().__init__() self.maingo = maingo self.initUI() def initUI(self): self.move(0, 0) self.setWindowTitle('Go') self.hb = QHBoxLayout() self.hb.setContentsMargins(0, 0, 0, 0) self.oldSizeConstraing = self.hb.sizeConstraint() self.stack = QStackedWidget() self.authorizeWidget = AuthorizeWidget(self.maingo, self) self.changeWidget(self.authorizeWidget) self.hb.addWidget(self.stack) self.setLayout(self.hb) self.show() def changeWidget(self, widget, oldsize=True): if oldsize: self.hb.setSizeConstraint(self.oldSizeConstraing) else: self.hb.setSizeConstraint(QLayout.SetFixedSize) if self.stack.currentWidget() != 0: self.stack.removeWidget(self.stack.currentWidget()) self.stack.addWidget(widget) self.stack.setCurrentWidget(widget)
class PyMultiPageWidget(QWidget): def __init__(self, parent=None): super(PyMultiPageWidget, self).__init__(parent) self.value = 0 self.bt = QPushButton('switch', self) self.bt.clicked.connect(self.switch) self.stackWidget = QStackedWidget() self.layout = QVBoxLayout() self.layout.addWidget(self.bt) self.layout.addWidget(self.stackWidget) self.setLayout(self.layout) self.resize(500, 400) def switch(self): self.value = 1 - self.value bt = QPushButton('new') if self.value: self.addPage(bt) else: self.removePage(0) @pyqtSlot(QWidget) def addPage(self, page): index = self.stackWidget.count() page.setParent(self.stackWidget) self.stackWidget.insertWidget(index, page) @pyqtSlot(int) def removePage(self, index): widget = self.stackWidget.widget(index) self.stackWidget.removeWidget(widget)
class IPFSHashExplorerStack(GalacteekTab): """ Organizes IPFSHashExplorerWidgets with a QStackedWidget """ def __init__(self, gWindow, hashRef, maxItems=16, parent=None): super(IPFSHashExplorerStack, self).__init__(gWindow) self.rootHash = hashRef self.maxItems = maxItems self.stack = QStackedWidget(self) self.exLayout = QVBoxLayout() self.exLayout.addWidget(self.stack) self.vLayout.addLayout(self.exLayout) if self.rootHash: self.viewHash(self.rootHash) def tabDestroyedPost(self): self.stack.setParent(None) self.stack.deleteLater() @property def itemsCount(self): return self.stack.count() def viewHash(self, hashRef, addClose=False, autoOpenFolders=False, parentView=None): view = IPFSHashExplorerWidget(hashRef, parent=self, addClose=addClose, showCidLabel=True, autoOpenFolders=autoOpenFolders, parentView=parentView) view.closeRequest.connect(partialEnsure(self.remove, view)) view.directoryOpenRequest.connect(lambda nView, cid: self.viewHash( cid, addClose=True, parentView=nView)) self.stack.insertWidget(self.stack.count(), view) self.stack.setCurrentWidget(view) view.reFocus() return True async def remove(self, view): try: view.cancelTasks() self.stack.removeWidget(view) except: pass async def onClose(self): for idx in range(self.stack.count()): widget = self.stack.widget(idx) await widget.cleanup() return True
class View_Menu(QWidget): #Initialiaze the first page for class View_Menu_First_Page() def __init__(self,parent=None): super(View_Menu,self).__init__(parent) self.resize(800,600) self.stackedwidget = QStackedWidget(self) self.stackedwidget.setGeometry(QtCore.QRect(0,0 , 800, 600)) self.stackedwidget.setObjectName("stackwidget") self.add_first_page() #function to show first page def add_first_page(self): self.first_page = View_Menu_First_Page() self.stackedwidget.addWidget(self.first_page) self.stackedwidget.setCurrentIndex(0) self.first_page.label_malay.clicked.connect(lambda:self.add_second_page(self.first_page.MY_link_click())) #only can use lambda: to make use of return value form functions self.first_page.label_mcdonalds.clicked.connect(lambda:self.add_second_page(self.first_page.MC_link_click())) #only can use lambda: to make use of return value form functions self.first_page.label_kfc.clicked.connect(lambda:self.add_second_page(self.first_page.KFC_link_click())) #only can use lambda: to make use of return value form functions self.first_page.label_subway.clicked.connect(lambda:self.add_second_page(self.first_page.Subway_link_click())) #only can use lambda: to make use of return value form functions #function to show second page def add_second_page(self,store): self.second_page = View_Menu_Second_Page(store) self.stackedwidget.addWidget(self.second_page) self.stackedwidget.setCurrentIndex(1) if 'Subway' in self.second_page.Logo.text(): print('Logo') self.second_page.BlueButton.clicked.connect(lambda:self.add_third_page(self.first_page.Subway_link_click(),self.second_page.SearchMenu())) elif 'Malay' in self.second_page.Logo.text(): print('Malay logo') self.second_page.BlueButton.clicked.connect(lambda:self.add_third_page(self.first_page.MY_link_click(),self.second_page.SearchMenu())) elif 'Mcdonalds' in self.second_page.Logo.text(): print('Mcdonalds Logo') self.second_page.BlueButton.clicked.connect(lambda:self.add_third_page(self.first_page.Mc_link_click(),self.second_page.SearchMenu())) elif 'KFC' in self.second_page.Logo.text(): print('KFC Logo') self.second_page.BlueButton.clicked.connect(lambda:self.add_third_page(self.first_page.KFC_link_click(),self.second_page.SearchMenu())) #function to show third page def add_third_page(self,store,date_and_time): self.third_page = View_Menu_Third_Page(store,date_and_time) self.stackedwidget.addWidget(self.third_page) self.stackedwidget.setCurrentIndex(2) #kfcCombine.KFC_breakfast_window.BlueButton_2.clicked.connect(self.go_back) self.third_page.BlueButton_2.clicked.connect(self.go_back) def go_back(self): self.stackedwidget.setCurrentIndex(0) self.stackedwidget.removeWidget(self.second_page) self.stackedwidget.removeWidget(self.third_page)
class receipt_view(QWidget, person_change_listener): __receipt_printouts = None __receipt_stack = None __person_list = None __model = None def __init__(self, model): super().__init__() self.__model = model self.__model.add_person_change_listeners(self) self.__receipt_printouts = list() # self.setWindowTitle("Receipt Tracker") self.__person_list = QComboBox() self.__person_list.adjustSize() self.__person_list.currentIndexChanged.connect(self.display) self.__receipt_stack = QStackedWidget() # for name in self.__model.get_names(): # printout = receipt_printout(name, self.__model) # self.__receipt_printouts.add(printout) # self.__receipt_stack.addWidget(printout) # self.__person_list.addItem(name) receipt_layout = QVBoxLayout() receipt_layout.addWidget(self.__person_list) receipt_layout.addWidget(self.__receipt_stack) self.setLayout(receipt_layout) self.show() def display(self, i): self.__receipt_stack.setCurrentIndex(i) def person_update(self, model): for printout in self.__receipt_printouts: self.__receipt_stack.removeWidget(printout) for name in model.get_names(): in_set = False for printout in self.__receipt_printouts: if name == printout.name: in_set = True if not in_set: self.__receipt_printouts.append(receipt_printout(name, model)) self.__person_list.addItem(name) for printout in self.__receipt_printouts: self.__receipt_stack.addWidget(printout)
class MainWindow(QWidget): def __init__(self): super(MainWindow, self).__init__() self.title_widget = TitleWidget() authorization_widget = AuthorizationWidget() authorization_widget.authorized.connect(self.on_authorized) self.main_stacked = QStackedWidget() self.main_stacked.setFixedSize(400, 260) self.main_stacked.addWidget(authorization_widget) self.footer_widget = FooterWidget() self.main_layout = QGridLayout() self.main_layout.setContentsMargins(24, 12, 24, 12) self.main_layout.addWidget(self.title_widget, 0, 0, Qt.AlignTop) self.main_layout.addWidget(self.main_stacked, 1, 0, Qt.AlignCenter) self.main_layout.addWidget(self.footer_widget, 2, 0, Qt.AlignBottom) self.setLayout(self.main_layout) self.resize(480, 320) def mousePressEvent(self, a0) -> None: self.setFocus() @pyqtSlot() def on_authorized(self): broadcast_setup_widget = BroadcastSetupWidget() broadcast_setup_widget.broadcast_created.connect(self.on_broadcast_created) self.switch_to_widget(broadcast_setup_widget) @pyqtSlot(Broadcast) def on_broadcast_created(self, broadcast: Broadcast): broadcast_ready_widget = BroadcastReadyWidget(broadcast) broadcast_ready_widget.new_broadcast.connect(self.on_authorized) self.switch_to_widget(broadcast_ready_widget) def switch_to_widget(self, widget: QWidget): focus_widget = self.main_stacked.focusWidget() self.main_stacked.setCurrentIndex(self.main_stacked.addWidget(widget)) self.main_stacked.removeWidget(focus_widget) focus_widget.deleteLater()
class IPFSHashExplorerStack(GalacteekTab): """ Organizes IPFSHashExplorerWidgets with a QStackedWidget """ def __init__(self, gWindow, hashRef, maxItems=16, parent=None): super(IPFSHashExplorerStack, self).__init__(gWindow) self.rootHash = hashRef self.maxItems = maxItems self.stack = QStackedWidget(self) self.exLayout = QVBoxLayout() self.exLayout.addWidget(self.stack) self.vLayout.addLayout(self.exLayout) if self.rootHash: self.viewHash(self.rootHash) @property def itemsCount(self): return self.stack.count() def viewHash(self, hashRef, addClose=False, autoOpenFolders=False): view = IPFSHashExplorerWidget(hashRef, parent=self, addClose=addClose, showCidLabel=True, autoOpenFolders=autoOpenFolders) view.closeRequest.connect(functools.partial(self.remove, view)) view.directoryOpenRequest.connect( lambda cid: self.viewHash(cid, addClose=True)) self.stack.insertWidget(self.stack.count(), view) self.stack.setCurrentWidget(view) view.reFocus() return True def remove(self, view): try: self.stack.removeWidget(view) except: pass
class MainWindow(QWidget): def __init__(self): super().__init__() layout = QVBoxLayout() self.stackedWidget = QStackedWidget() self.stackedWidget.addWidget(ClockPanel()) self.alarms_panel = AlarmsPanel() self.stackedWidget.addWidget(self.alarms_panel) self.stackedWidget.addWidget(SettingsPanel()) layout.addWidget(self.stackedWidget) menu_buttons = MenuButtons() menu_buttons.switch_panel.connect(self.switch_panel) layout.addWidget(menu_buttons) self.alarms_panel.edit_alarms.connect(self.display_edit_alarm) self.setLayout(layout) @pyqtSlot(int, name="switch_panel") def switch_panel(self, panel_index): self.stackedWidget.setCurrentIndex(panel_index) @pyqtSlot(int, name="edit_alarm") def display_edit_alarm(self, alarm_id): print('EDIT ' + str(alarm_id)) # drop alarms panel edit_alarm_panel = EditAlarmPanel(alarm_id) self.stackedWidget.insertWidget(1, edit_alarm_panel) self.stackedWidget.setCurrentIndex(1) self.stackedWidget.removeWidget(self.alarms_panel) # display edit alarm panel edit_alarm_panel.quit_signal.connect(self.hello) @pyqtSlot(name="quit") def hello(self): print('should close the edit alarm panel')
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self): super().__init__() self.setupUi(self) self.stacked_widget = QStackedWidget() game_mode_window = GameModeWindow(self) options_menu_window = OptionsMenuWindow(self) versus_opts_window = VersusGameOptsWindow(self) self.stacked_widget.addWidget(self.centralwidget) self.stacked_widget.addWidget(game_mode_window) self.stacked_widget.addWidget(options_menu_window) self.stacked_widget.addWidget(versus_opts_window) self.stacked_widget.setCurrentIndex(0) layout = QtWidgets.QVBoxLayout() layout.addWidget(self.stacked_widget) central_widget = QtWidgets.QWidget(self) central_widget.setLayout(layout) self.setCentralWidget(central_widget) self.buttonStart.clicked.connect( lambda: self.stacked_widget.setCurrentIndex(1)) self.buttonOpts.clicked.connect( lambda: self.stacked_widget.setCurrentIndex(2)) self.buttonExit.clicked.connect(self.close) self.setWindowTitle("chessVshogi") self.show() def closeEvent(self, event): current_index = self.stacked_widget.currentIndex() if current_index != 0: if current_index == 4: del self.stacked_widget.currentWidget().state self.stacked_widget.removeWidget( self.stacked_widget.currentWidget()) self.stacked_widget.setCurrentIndex(0) self.setWindowTitle("chessVshogi") event.ignore() else: event.accept()
class ReaderWindow(QMainWindow): def __init__(self): super().__init__() # Main body self.bodyQHBoxLayout = QVBoxLayout() self.body = QWidget() self.stack = QStackedWidget() self.stack.addWidget(self.body) # Container self.containerQHBoxLayout = QHBoxLayout() self.containerQHBoxLayout.setSpacing(50) self.container = QWidget() # SideBar self.sidebarQVBoxLayout = QVBoxLayout() self.sidebar = QWidget() self.sidebar.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) # Back Button self.backQButton = QPushButton() self.backQButton.setIcon(QIcon('design/images/back.png')) self.backQButton.setIconSize(QSize(32, 32)) self.backQButton.setFocusPolicy(Qt.NoFocus) self.backQButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) styles.Styles.set_back_button_styles(self.backQButton) # Button menu self.buttonMenuQVBoxLayout = QVBoxLayout() self.buttonMenuQVBoxLayout.setContentsMargins(0, 0, 0, 0) self.buttonMenu = QWidget() # Fullscreen Button self.fullscreenQButton = QPushButton() self.fullscreenQButton.setIcon(QIcon('design/images/fullscreen.png')) self.fullscreenQButton.setIconSize(QSize(32, 32)) self.fullscreenQButton.setFocusPolicy(Qt.NoFocus) # Zoom In Button self.zoomInQButton = QPushButton() self.zoomInQButton.setIcon(QIcon('design/images/zoom-in.png')) self.zoomInQButton.setIconSize(QSize(32, 32)) self.zoomInQButton.setFocusPolicy(Qt.NoFocus) # Zoom Out Button self.zoomOutQButton = QPushButton() self.zoomOutQButton.setIcon(QIcon('design/images/zoom-out.png')) self.zoomOutQButton.setIconSize(QSize(32, 32)) self.zoomOutQButton.setFocusPolicy(Qt.NoFocus) # Show Content Button self.contentQButton = QPushButton() self.contentQButton.setIcon(QIcon('design/images/content.png')) self.contentQButton.setIconSize(QSize(32, 32)) self.contentQButton.setFocusPolicy(Qt.NoFocus) self.contentQButton.clicked.connect(self.show_content) # Add Bookmarks Button self.addBookmarkQButton = QPushButton() self.addBookmarkQButton.setIcon(QIcon('design/images/bookmark.png')) self.addBookmarkQButton.setIconSize(QSize(32, 32)) self.addBookmarkQButton.setFocusPolicy(Qt.NoFocus) # Add Notes Button self.addNoteQButton = QPushButton() self.addNoteQButton.setIcon(QIcon('design/images/notes.png')) self.addNoteQButton.setIconSize(QSize(32, 32)) self.addNoteQButton.setFocusPolicy(Qt.NoFocus) # Switch mode Button self.switchModeQButton = QPushButton() self.switchModeQButton.setIcon(QIcon('design/images/switch.png')) self.switchModeQButton.setIconSize(QSize(32, 32)) self.switchModeQButton.setFocusPolicy(Qt.NoFocus) # Content self.contentQVBoxLayout = QVBoxLayout() self.content = QWidget() self.content.setStyleSheet(""" background-color: white; """) # Content window self.content_window = None # Footer self.footerQHBoxLayout = QHBoxLayout() self.footer = QWidget() self.footer.setContentsMargins(11, 0, 0, 0) # Show progress Label self.progressLabel = QLabel("100% ") self.progressLabel.setAlignment(Qt.AlignCenter) # Switch page block self.switchPageQHBoxLayout = QHBoxLayout() self.switchPageWidget = QWidget() self.switchPageQHBoxLayout.setSpacing(0) # Previous Page Button self.previousPageQButton = QPushButton() self.previousPageQButton.setIcon(QIcon('design/images/back.png')) self.previousPageQButton.setIconSize(QSize(32, 32)) self.previousPageQButton.setFocusPolicy(Qt.NoFocus) # Next Page Button self.nextPageQButton = QPushButton() self.nextPageQButton.setIcon(QIcon('design/images/front.png')) self.nextPageQButton.setIconSize(QSize(32, 32)) self.nextPageQButton.setFocusPolicy(Qt.NoFocus) styles.Styles.set_window_styles(self) localize.set_reader_localization(self) self.init_body() def init_sidebar(self): self.sidebarQVBoxLayout.addWidget(self.backQButton) self.buttonMenuQVBoxLayout.addWidget(self.fullscreenQButton) self.buttonMenuQVBoxLayout.addWidget(self.zoomInQButton) self.buttonMenuQVBoxLayout.addWidget(self.zoomOutQButton) self.buttonMenuQVBoxLayout.addWidget(self.contentQButton) self.buttonMenuQVBoxLayout.addWidget(self.addBookmarkQButton) self.buttonMenuQVBoxLayout.addWidget(self.addNoteQButton) self.buttonMenuQVBoxLayout.addWidget(self.switchModeQButton) self.buttonMenu.setLayout(self.buttonMenuQVBoxLayout) self.sidebarQVBoxLayout.addWidget(self.buttonMenu) self.sidebarQVBoxLayout.addStretch() self.sidebarQVBoxLayout.setSpacing(80) self.sidebar.setLayout(self.sidebarQVBoxLayout) def init_content(self): self.content.setLayout(self.contentQVBoxLayout) def init_container(self): self.init_sidebar() self.init_content() self.containerQHBoxLayout.addWidget(self.sidebar) self.containerQHBoxLayout.addWidget(self.content) self.container.setLayout(self.containerQHBoxLayout) def init_footer(self): self.footerQHBoxLayout.addWidget(self.progressLabel) self.switchPageQHBoxLayout.addWidget(self.previousPageQButton) self.switchPageQHBoxLayout.addWidget(self.nextPageQButton) self.switchPageWidget.setLayout(self.switchPageQHBoxLayout) self.footerQHBoxLayout.addStretch() self.footerQHBoxLayout.addWidget(self.switchPageWidget) self.footerQHBoxLayout.addStretch() self.footer.setLayout(self.footerQHBoxLayout) def init_body(self): self.init_container() self.init_footer() self.bodyQHBoxLayout.addWidget(self.container) self.bodyQHBoxLayout.addWidget(self.footer) self.body.setLayout(self.bodyQHBoxLayout) self.setCentralWidget(self.stack) def show_content(self): self.stack.addWidget(self.create_content_widget()) self.stack.setCurrentIndex(1) def hide_content(self): self.stack.setCurrentIndex(0) self.stack.removeWidget(self.content_window) self.content_window = None def create_content_widget(self): self.content_window = content_win.ContentWindow() self.content_window.backQButton.clicked.connect(self.hide_content) return self.content_window
class GstMediaSettings(SettingsSection): Name = 'Media Settings' def __init__(self, size, cue=None, parent=None): super().__init__(size, cue=cue, parent=parent) self._pipe = '' self._conf = {} self._check = False self.glayout = QGridLayout(self) self.listWidget = QListWidget(self) self.glayout.addWidget(self.listWidget, 0, 0) self.pipeButton = QPushButton('Change Pipe', self) self.glayout.addWidget(self.pipeButton, 1, 0) self.elements = QStackedWidget(self) self.glayout.addWidget(self.elements, 0, 1, 2, 1) self.glayout.setColumnStretch(0, 2) self.glayout.setColumnStretch(1, 5) self.listWidget.currentItemChanged.connect(self.__change_page) self.pipeButton.clicked.connect(self.__edit_pipe) def set_configuration(self, conf): # Get the media section of the cue configuration if conf is not None: conf = conf.get('media', {}) # Activate the layout, so we can get the right widgets size self.glayout.activate() # Create a local copy of the configuration self._conf = deepcopy(conf) # Create the widgets sections = sections_by_element_name() for element in conf.get('pipe', '').split('!'): widget = sections.get(element) if widget is not None: widget = widget(self.elements.size(), element, self) widget.set_configuration(self._conf['elements']) self.elements.addWidget(widget) item = QListWidgetItem(widget.NAME) self.listWidget.addItem(item) self.listWidget.setCurrentRow(0) def get_configuration(self): conf = {'elements': {}} for el in self.elements.children(): if isinstance(el, SettingsSection): conf['elements'].update(el.get_configuration()) # If in check mode the pipeline is not returned if not self._check: conf['pipe'] = self._conf['pipe'] return {'media': conf} def enable_check(self, enable): self._check = enable for element in self.elements.children(): if isinstance(element, SettingsSection): element.enable_check(enable) def __change_page(self, current, previous): if not current: current = previous self.elements.setCurrentIndex(self.listWidget.row(current)) def __edit_pipe(self): # Backup the settings self._conf.update(self.get_configuration()['media']) # Show the dialog dialog = GstPipeEdit(self._conf.get('pipe', ''), parent=self) if dialog.exec_() == dialog.Accepted: # Reset the view for _ in range(self.elements.count()): self.elements.removeWidget(self.elements.widget(0)) self.listWidget.clear() # Reload with the new pipeline self._conf['pipe'] = dialog.get_pipe() self.set_configuration({'media': self._conf}) self.enable_check(self._check)
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.gameConstructor = GameConstructor( self, MyAction("Back", self.action_quit_from_game)) self.gameParams = GameParams.DefaultValues.create() self.initializase_components() self.state_menu() self.show() def initializase_components(self): self.stckWidg_scene = QStackedWidget() self.setCentralWidget(self.stckWidg_scene) self.menuScene = QWid_MenuScene(self, [ MyAction("Play", self.action_play), MyAction("Rules", self.action_rules), MyAction("Settings", self.action_settings), MyAction("Quit", self.action_quit) ]) self.rulesScene = QWid_RulesScene(self, MyAction("Back", self.state_menu)) self.settingsScene = QWid_SettingsScene( self, MyAction("Back", self.action_quit_from_settings)) self.stckWidg_scene.addWidget(self.settingsScene) self.stckWidg_scene.addWidget(self.menuScene) self.stckWidg_scene.addWidget(self.rulesScene) def createGameScene(self): return self.gameConstructor.createGame(self.gameParams) #return QMWind_GameScene(self, MyAction("Back", self.action_quit_from_game), self.gamePrefs) def action_play(self): self.state_game() def action_settings(self): self.state_settings() def action_rules(self): self.state_rules() def action_quit(self): self.close() def action_quit_from_game(self): self.state_menu() self.stckWidg_scene.removeWidget(self.gameScene) def action_quit_from_settings(self): self.gameParams = self.settingsScene.getChangedGameParams() self.state_menu() def state_game(self): self.gameScene = self.createGameScene() self.stckWidg_scene.addWidget(self.gameScene) self.gameScene.onShow() self.stckWidg_scene.setCurrentWidget(self.gameScene) def state_menu(self): self.menuScene.onShow() self.stckWidg_scene.setCurrentWidget(self.menuScene) def state_rules(self): self.rulesScene.onShow() self.stckWidg_scene.setCurrentWidget(self.rulesScene) def state_settings(self): self.settingsScene.onShow(self.gameParams) self.stckWidg_scene.setCurrentWidget(self.settingsScene)
class TableWidget(QSplitter): def __init__(self): super(TableWidget, self).__init__() # vbox = QVBoxLayout(self) # vbox.setContentsMargins(0, 0, 0, 0) self._tabs = QTabWidget() self._tabs.setAutoFillBackground(True) p = self._tabs.palette() p.setColor(p.Window, QColor("white")) self._tabs.setPalette(p) self._other_tab = QTabWidget() self._other_tab.setAutoFillBackground(True) self._other_tab.setPalette(p) self.addWidget(self._tabs) self.addWidget(self._other_tab) self.setSizes([1, 1]) self._other_tab.hide() self.relations = {} # Stack self.stacked = QStackedWidget() self._tabs.addTab(self.stacked, "Workspace") self.stacked_result = QStackedWidget() self._tabs.addTab(self.stacked_result, self.tr("Resultados")) btn_split = QToolButton() btn_split.setToolTip(self.tr("Click para dividir la pantalla")) btn_split.setAutoRaise(True) btn_split.setIcon(QIcon(":img/split")) self._tabs.setCornerWidget(btn_split) btn_split.clicked.connect(self._split) btn_split = QToolButton() btn_split.setToolTip(self.tr("Click para juntar las pantallas")) btn_split.setAutoRaise(True) btn_split.setIcon(QIcon(":img/split")) btn_split.clicked.connect(self._unsplit) self._other_tab.setCornerWidget(btn_split) # self.setContextMenuPolicy(Qt.CustomContextMenu) # self.customContextMenuRequested.connect(self._show_menu) lateral_widget = Pireal.get_service("lateral_widget") lateral_widget.resultClicked.connect(self._on_result_list_clicked) lateral_widget.resultSelectionChanged.connect( lambda index: self.stacked_result.setCurrentIndex(index)) # lateral_widget.newRowsRequested.connect(self._insert_rows) def insert_rows(self, tuplas): current_view = self.current_table() if current_view is not None: model = current_view.model() for tupla in tuplas: model.insertRow(model.rowCount(), tupla) current_view.adjust_columns() def _on_result_list_clicked(self, index): self.stacked_result.setCurrentIndex(index) if not self._other_tab.isVisible(): self._tabs.setCurrentIndex(1) def _unsplit(self): self._other_tab.hide() result_widget = self._other_tab.widget(0) self._tabs.addTab(result_widget, self.tr("Resultados")) self._tabs.cornerWidget().show() def _split(self): result_widget = self._tabs.widget(1) self._other_tab.addTab(result_widget, self.tr("Resultados")) self._other_tab.show() self.setSizes([1, 1]) self._tabs.cornerWidget().hide() self.setOrientation(Qt.Horizontal) def _show_menu(self, position): menu = QMenu(self) if self.count() > 0: add_tuple_action = menu.addAction(self.tr("Agregar Tupla")) add_col_action = menu.addAction(self.tr("Add Column")) add_tuple_action.triggered.connect(self.add_tuple) add_col_action.triggered.connect(self.add_column) menu.addSeparator() add_relation_action = menu.addAction(self.tr("Create new Relation")) add_relation_action.triggered.connect(self.__new_relation) menu.exec_(self.mapToGlobal(position)) def __new_relation(self): central_service = Pireal.get_service("central") central_service.create_new_relation() def count(self): return self.stacked.count() def remove_table(self, index): widget = self.stacked.widget(index) self.stacked.removeWidget(widget) del widget def current_table(self): return self.stacked.currentWidget() def remove_relation(self, name): del self.relations[name] def add_relation(self, name, rela): if self.relations.get(name, None) is None: self.relations[name] = rela return True return False def add_table(self, rela, name, table): """ Add new table from New Relation Dialog """ self.add_relation(name, rela) self.stacked.addWidget(table) def add_tuple(self): current_view = self.current_table() if current_view is not None: model = current_view.model() model.insertRow(model.rowCount()) def add_column(self): current_view = self.current_table() if current_view is not None: model = current_view.model() model.insertColumn(model.columnCount()) def delete_tuple(self): current_view = self.current_table() if current_view is not None: model = current_view.model() selection = current_view.selectionModel() if selection.hasSelection(): selection = selection.selection() rows = set([index.row() for index in selection.indexes()]) rows = sorted(list(rows)) previous = -1 i = len(rows) - 1 while i >= 0: current = rows[i] if current != previous: model.removeRow(current) i -= 1 def delete_column(self): """ Elimina la/las columnas seleccionadas """ current_view = self.current_table() if current_view is not None: model = current_view.model() selection = current_view.selectionModel() if selection.hasSelection(): selection = selection.selection() columns = set( [index.column() for index in selection.indexes()]) columns = sorted(list(columns)) previous = -1 i = len(columns) - 1 while i >= 0: current = columns[i] if current != previous: model.removeColumn(current) i -= 1 def create_table(self, rela, editable=True): """ Se crea la vista y el modelo """ _view = view.View() _model = model.Model(rela) if not editable: _model.editable = False _view.setModel(_model) _view.setItemDelegate(delegate.Delegate()) _view.setHorizontalHeader(view.Header()) return _view
class MainWindow(QMainWindow): screen = None width, height = None, None picture = None login_form = None user_page = None pages = None def __init__(self): super().__init__() if sys.platform != 'linux': my_app_id = 'InnoUI.DMD_project.ez_A_for_course.101' # arbitrary string ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( my_app_id) self.setWindowIcon(QIcon("./images/main_logo.png")) size = QDesktopWidget().screenGeometry(-1) self.width = size.width() // 5 self.picture = QPixmap("./images/main_logo.png").scaledToWidth( self.width - self.width // 20) self.height = self.picture.height() + size.height() // 16 self.setFixedSize(self.width, self.height) self.setWindowTitle('DmD2') self.pages = QStackedWidget() qr = self.frameGeometry() center = QDesktopWidget().availableGeometry().center() qr.moveCenter(center) self.move(qr.topLeft()) self.init_ui() def login(self, login, password): try: backend.login(login, password) except AssertionError as exception: QToolTip.showText( self.mapToGlobal(self.login_form.geometry().center()), 'Invalid credentials') return self.user_page = levels[backend.user.user_info['role']](self, backend.user) self.pages.addWidget(self.user_page) self.pages.setCurrentWidget(self.user_page) self.repaint() self.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) self.resize(self.width * 2, self.height * 2) qr = self.frameGeometry() center = QDesktopWidget().availableGeometry().center() qr.moveCenter(center) self.move(qr.topLeft()) def logout(self): backend.logout() self.pages.setCurrentWidget(self.login_form) self.repaint() self.setFixedSize(self.width, self.height) qr = self.frameGeometry() center = QDesktopWidget().availableGeometry().center() qr.moveCenter(center) self.move(qr.topLeft()) self.pages.removeWidget(self.user_page) def init_ui(self): self.login_form = LoginForm(self.width, self.height, self.picture) self.user_page = BasePage(self) self.pages.addWidget(self.login_form) self.pages.setCurrentWidget(self.login_form) self.setCentralWidget(self.pages) self.login_form.button.clicked.connect( lambda: self.login(self.login_form.login_field.text(), self.login_form.password_field.text())) self.show()
class ClientGui(QWidget): def __init__(self): self.service_msg_deq = [] # очередь сервисных сообщений, очищать! self.icon_user = QIcon("user.svg") self.icon_new_msg = QIcon("message.svg") super().__init__() self.get_login_dialog() def initUI(self): # Кнопки: добавить/удалить контакты в контакт лист self.button_add_contact = QPushButton('add', self) self.button_add_contact.clicked.connect(self.add_contact) self.button_del_contact = QPushButton('del', self) self.button_del_contact.clicked.connect(self.del_contact) self.button_settings = QPushButton('men', self) self.button_settings.setEnabled(False) # не работает self.button_connect = QPushButton('conn', self) self.button_connect.setEnabled(False) # не работает self.box_button = QHBoxLayout() self.box_button.addWidget(self.button_add_contact) self.box_button.addWidget(self.button_del_contact) self.box_button.addWidget(self.button_settings) self.box_button.addWidget(self.button_connect) # создаю модель для листа контактов, подключаю отображение cl = bd_client_app.BDContacts().get_contacts() self.model_cl = QStandardItemModel() for user in cl: row = QStandardItem(self.icon_user, user) self.model_cl.appendRow(row) self.contact_list = QListView() self.contact_list.setModel(self.model_cl) self.contact_list.setSelectionMode(QListView.SingleSelection) self.contact_list.setEditTriggers(QListView.NoEditTriggers) self.contact_list.clicked.connect(self.select_conlist) # строка и кнопка отправки сообщений qButton = QPushButton('>>', self) qButton.clicked.connect(self.send_click) self.sendBox = QLineEdit(self) self.sendBox.returnPressed.connect(self.send_click) self.messageBox = QStackedWidget() # два словаря, в первом: логин ключ виджет значение, второй наоборот self.messageBox_dict_ctw = {} self.messageBox_dict_wtc = {} for user in cl: self.messageBox_dict_ctw[user] = QListWidget() self.messageBox_dict_wtc[self.messageBox_dict_ctw[user]] = user self.messageBox.addWidget(self.messageBox_dict_ctw[user]) grid = QGridLayout() # строка, столбец, растянуть на строк, растянуть на столбцов grid.addWidget(self.contact_list, 0, 0, 2, 3) grid.addLayout(self.box_button, 2, 0) grid.addWidget(self.messageBox, 0, 3, 2, 3) grid.addWidget(self.sendBox, 2, 3, 1, 2) grid.addWidget(qButton, 2, 5) grid.setSpacing(5) grid.setColumnMinimumWidth(3, 200) grid.setColumnStretch(3, 10) self.setLayout(grid) self.resize(800, 300) self.center() self.setWindowTitle('Avocado') self.setWindowIcon(QIcon('icon.svg')) self.show() def initThreads(self): self.print_thread = ClientThreads(self.client, self) self.print_thread.print_signal.connect(self.add_message) self.print_thread.start() def get_login_dialog(self): text, ok = QInputDialog.getText(self, 'Login', 'Connect with login:') self.login_name = str(text) if ok: self.service_msg_deq.clear() # жду свой ответ self.init_client() self.initThreads() # while not self.service_msg_deq: # print(self.service_msg_deq) # pass # жду ответ # if self.service_msg_deq[0] is True: time.sleep(1) self.initUI() # else: # self.exit() else: self.exit() def init_client(self): self.client = client.Client(self.login_name, "localhost", 7777) self.client.start_th_gui_client() def center(self): # центрирую окно qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) @pyqtSlot() def send_click(self): text_to_send = self.sendBox.text() if text_to_send.rstrip(): self.messageBox.currentWidget().addItem("<< " + text_to_send) self.client.inp_queue.put(text_to_send) self.sendBox.clear() @pyqtSlot(QModelIndex) def select_conlist(self, curr): self.messageBox.setCurrentIndex(curr.row()) self.model_cl.itemFromIndex(curr).setIcon(self.icon_user) self.client.to_user = self.messageBox_dict_wtc[ self.messageBox.currentWidget()] @pyqtSlot(tuple) def add_message(self, message): msg = message[0] from_u = message[1] try: client_widget = self.messageBox_dict_ctw[from_u] except KeyError: mesg_con_log.error("Message from user from not in contact list:") mesg_con_log.error("%s, %s" % (from_u, msg)) else: client_widget.addItem(">> " + msg) message_from = self.model_cl.findItems(from_u)[0] if self.contact_list.currentIndex() != self.model_cl.indexFromItem( message_from): message_from.setIcon(self.icon_new_msg) @pyqtSlot() def del_contact(self): user = self.client.to_user self.client.inp_queue.put("del_contact " + user) self.messageBox.removeWidget(self.messageBox_dict_ctw[user]) self.model_cl.takeRow( self.model_cl.indexFromItem( self.model_cl.findItems(user)[0]).row()) @pyqtSlot() def add_contact(self): user = self.sendBox.text() self.service_msg_deq.clear() # жду свой ответ self.client.inp_queue.put("add_contact " + user) while not self.service_msg_deq: pass # жду ответ if self.service_msg_deq[0] is True: row = QStandardItem(self.icon_user, user) self.model_cl.appendRow(row) self.messageBox_dict_ctw[user] = QListWidget() self.messageBox_dict_wtc[self.messageBox_dict_ctw[user]] = user self.messageBox.addWidget(self.messageBox_dict_ctw[user]) else: pass self.sendBox.clear()
class SubTabWidget(QWidget): _tabChanged = pyqtSignal(int, name = "tabChanged") def __init__(self, subtitleData, videoWidget, parent = None): super(SubTabWidget, self).__init__(parent) self._subtitleData = subtitleData self.__initTabWidget(videoWidget) def __initTabWidget(self, videoWidget): settings = SubSettings() mainLayout = QVBoxLayout(self) mainLayout.setContentsMargins(0, 0, 0, 0) mainLayout.setSpacing(0) #TabBar self.tabBar = QTabBar(self) # Splitter (bookmarks + pages) self.splitter = QSplitter(self) self.splitter.setObjectName("sidebar_splitter") self._toolbox = ToolBox(self._subtitleData, self) self._toolbox.setObjectName("sidebar") self._toolbox.setMinimumWidth(100) self._toolbox.addTool(Details(self._subtitleData, self)) self._toolbox.addTool(Synchronizer(videoWidget, self._subtitleData, self)) self._toolbox.addTool(History(self)) self.rightWidget = QWidget() rightLayout = QGridLayout() rightLayout.setContentsMargins(0, 0, 0, 0) self.rightWidget.setLayout(rightLayout) self._mainTab = FileList(_("Subtitles"), self._subtitleData, self) self.pages = QStackedWidget(self) rightLayout.addWidget(self.pages, 0, 0) self.tabBar.addTab(self._mainTab.name) self.pages.addWidget(self._mainTab) self.splitter.addWidget(self._toolbox) self.splitter.addWidget(self.rightWidget) self.__drawSplitterHandle(1) # Setting widgets mainLayout.addWidget(self.tabBar) mainLayout.addWidget(self.splitter) # Widgets settings self.tabBar.setMovable(True) self.tabBar.setTabsClosable(True) self.tabBar.setExpanding(False) # Don't resize left panel if it's not needed leftWidgetIndex = self.splitter.indexOf(self._toolbox) rightWidgetIndex = self.splitter.indexOf(self.rightWidget) self.splitter.setStretchFactor(leftWidgetIndex, 0) self.splitter.setStretchFactor(rightWidgetIndex, 1) self.splitter.setCollapsible(leftWidgetIndex, False) self.splitter.setSizes([250]) # Some signals self.tabBar.currentChanged.connect(self.showTab) self.tabBar.tabCloseRequested.connect(self.closeTab) self.tabBar.tabMoved.connect(self.moveTab) self._mainTab.requestOpen.connect(self.openTab) self._mainTab.requestRemove.connect(self.removeFile) self.tabChanged.connect(lambda i: self._toolbox.setContentFor(self.tab(i))) self.setLayout(mainLayout) def __addTab(self, filePath): """Returns existing tab index. Creates a new one if it isn't opened and returns its index otherwise.""" for i in range(self.tabBar.count()): widget = self.pages.widget(i) if not widget.isStatic and filePath == widget.filePath: return i tab = SubtitleEditor(filePath, self._subtitleData, self) newIndex = self.tabBar.addTab(self._createTabName(tab.name, tab.history.isClean())) tab.history.cleanChanged.connect( lambda clean: self._cleanStateForFileChanged(filePath, clean)) self.pages.addWidget(tab) return newIndex def __drawSplitterHandle(self, index): splitterHandle = self.splitter.handle(index) splitterLayout = QVBoxLayout(splitterHandle) splitterLayout.setSpacing(0) splitterLayout.setContentsMargins(0, 0, 0, 0) line = QFrame(splitterHandle) line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) splitterLayout.addWidget(line) splitterHandle.setLayout(splitterLayout) def _createTabName(self, name, cleanState): if cleanState is True: return name else: return "%s +" % name def _cleanStateForFileChanged(self, filePath, cleanState): page = self.tabByPath(filePath) if page is not None: for i in range(self.tabBar.count()): if self.tabBar.tabText(i)[:len(page.name)] == page.name: self.tabBar.setTabText(i, self._createTabName(page.name, cleanState)) return def saveWidgetState(self, settings): settings.setState(self.splitter, self.splitter.saveState()) settings.setHidden(self._toolbox, self._toolbox.isHidden()) def restoreWidgetState(self, settings): self.showPanel(not settings.getHidden(self._toolbox)) splitterState = settings.getState(self.splitter) if not splitterState.isEmpty(): self.splitter.restoreState(settings.getState(self.splitter)) @pyqtSlot(str, bool) def openTab(self, filePath, background=False): if self._subtitleData.fileExists(filePath): tabIndex = self.__addTab(filePath) if background is False: self.showTab(tabIndex) else: log.error(_("SubtitleEditor not created for %s!" % filePath)) @pyqtSlot(str) def removeFile(self, filePath): tab = self.tabByPath(filePath) command = RemoveFile(filePath) if tab is not None: index = self.pages.indexOf(tab) if self.closeTab(index): self._subtitleData.execute(command) else: self._subtitleData.execute(command) @pyqtSlot(int) def closeTab(self, index): tab = self.tab(index) if tab.canClose(): widgetToRemove = self.pages.widget(index) self.tabBar.removeTab(index) self.pages.removeWidget(widgetToRemove) widgetToRemove.deleteLater() return True return False def count(self): return self.tabBar.count() def currentIndex(self): return self.tabBar.currentIndex() def currentPage(self): return self.pages.currentWidget() @pyqtSlot(int, int) def moveTab(self, fromIndex, toIndex): fromWidget = self.pages.widget(fromIndex) toWidget = self.pages.widget(toIndex) if fromWidget.isStatic or toWidget.isStatic: self.tabBar.blockSignals(True) # signals would cause infinite recursion self.tabBar.moveTab(toIndex, fromIndex) self.tabBar.blockSignals(False) return else: self.pages.removeWidget(fromWidget) self.pages.removeWidget(toWidget) if fromIndex < toIndex: self.pages.insertWidget(fromIndex, toWidget) self.pages.insertWidget(toIndex, fromWidget) else: self.pages.insertWidget(toIndex, fromWidget) self.pages.insertWidget(fromIndex, toWidget) # Hack # Qt changes tabs during mouse drag and dropping. The next line is added # to prevent it. self.showTab(self.tabBar.currentIndex()) @pyqtSlot(int) def showTab(self, index): showWidget = self.pages.widget(index) if showWidget: self.pages.setCurrentWidget(showWidget) self.tabBar.blockSignals(True) self.tabBar.setCurrentIndex(index) self.tabBar.blockSignals(False) # Try to update current tab. showWidget.updateTab() self._tabChanged.emit(index) def showPanel(self, val): if val is True: self._toolbox.show() else: self._toolbox.hide() def togglePanel(self): if self._toolbox.isHidden(): self._toolbox.show() else: self._toolbox.hide() def tab(self, index): return self.pages.widget(index) def tabByPath(self, path): for i in range(self.pages.count()): page = self.tab(i) if not page.isStatic and page.filePath == path: return page return None @property def fileList(self): return self._mainTab
class PyMultiPageWidget(QWidget): currentIndexChanged = pyqtSignal(int) pageTitleChanged = pyqtSignal(str) def __init__(self, parent=None): super(PyMultiPageWidget, self).__init__(parent) self.comboBox = QComboBox() # MAGIC # It is important that the combo box has an object name beginning # with '__qt__passive_', otherwise, it is inactive in the form editor # of the designer and you can't change the current page via the # combo box. # MAGIC self.comboBox.setObjectName('__qt__passive_comboBox') self.stackWidget = QStackedWidget() self.comboBox.activated.connect(self.setCurrentIndex) self.layout = QVBoxLayout() self.layout.addWidget(self.comboBox) self.layout.addWidget(self.stackWidget) self.setLayout(self.layout) def sizeHint(self): return QSize(200, 150) def count(self): return self.stackWidget.count() def widget(self, index): return self.stackWidget.widget(index) @pyqtSlot(QWidget) def addPage(self, page): self.insertPage(self.count(), page) @pyqtSlot(int, QWidget) def insertPage(self, index, page): page.setParent(self.stackWidget) self.stackWidget.insertWidget(index, page) title = page.windowTitle() if title == "": title = "Page %d" % (self.comboBox.count() + 1) page.setWindowTitle(title) self.comboBox.insertItem(index, title) @pyqtSlot(int) def removePage(self, index): widget = self.stackWidget.widget(index) self.stackWidget.removeWidget(widget) self.comboBox.removeItem(index) def getPageTitle(self): return self.stackWidget.currentWidget().windowTitle() @pyqtSlot(str) def setPageTitle(self, newTitle): self.comboBox.setItemText(self.getCurrentIndex(), newTitle) self.stackWidget.currentWidget().setWindowTitle(newTitle) self.pageTitleChanged.emit(newTitle) def getCurrentIndex(self): return self.stackWidget.currentIndex() @pyqtSlot(int) def setCurrentIndex(self, index): if index != self.getCurrentIndex(): self.stackWidget.setCurrentIndex(index) self.comboBox.setCurrentIndex(index) self.currentIndexChanged.emit(index) pageTitle = pyqtProperty(str, fget=getPageTitle, fset=setPageTitle, stored=False) currentIndex = pyqtProperty(int, fget=getCurrentIndex, fset=setCurrentIndex)
class CentralWidget(QWidget): # This signals is used by notificator databaseSaved = pyqtSignal('QString') querySaved = pyqtSignal('QString') def __init__(self): QWidget.__init__(self) box = QVBoxLayout(self) box.setContentsMargins(0, 0, 0, 0) box.setSpacing(0) self.stacked = QStackedWidget() box.addWidget(self.stacked) self.created = False self.__last_open_folder = None self.__recent_dbs = [] if PSetting.RECENT_DBS: self.__recent_dbs = PSetting.RECENT_DBS Pireal.load_service("central", self) @property def recent_databases(self): return self.__recent_dbs @recent_databases.setter def recent_databases(self, database_file): if database_file in PSetting.RECENT_DBS: PSetting.RECENT_DBS.remove(database_file) PSetting.RECENT_DBS.insert(0, database_file) self.__recent_dbs = PSetting.RECENT_DBS def create_database(self): """ Show a wizard widget to create a new database, only have one database open at time. """ if self.created: QMessageBox.information(self, self.tr("Information"), self.tr("You may only have one database" " open at time.")) DEBUG("Ya existe una base de datos abierta") return wizard = database_wizard.DatabaseWizard(self) wizard.wizardFinished.connect( self.__on_wizard_finished) # Hide menubar and toolbar pireal = Pireal.get_service("pireal") pireal.show_hide_menubar() pireal.show_hide_toolbar() # Add wizard widget to stacked self.add_widget(wizard) def __on_wizard_finished(self, data, wizard_widget): """ This slot execute when wizard to create a database is finished """ pireal = Pireal.get_service("pireal") if not data: # If it's canceled, remove wizard widget and return to Start Page self.remove_last_widget() else: # Create a new data base container db_container = database_container.DatabaseContainer() # Associate the file name with the PFile object pfile_object = pfile.File(data['filename']) # Associate PFile object with data base container # and add widget to stacked db_container.pfile = pfile_object self.add_widget(db_container) # Remove wizard self.stacked.removeWidget(wizard_widget) # Set window title pireal.change_title(file_manager.get_basename(data['filename'])) # Enable db actions pireal.set_enabled_db_actions(True) pireal.set_enabled_relation_actions(True) self.created = True DEBUG("Base de datos creada correctamente: '{}'".format( data['filename'])) # If data or not, show menubar and toolbar again pireal.show_hide_menubar() pireal.show_hide_toolbar() def open_database(self, filename=''): """ This function opens a database and set this on the UI """ # If not filename provide, then open dialog to select if self.created: QMessageBox.information(self, self.tr("Information"), self.tr("You may only have one database" " open at time.")) DEBUG("Ya existe una base de datos abierta") return if not filename: if self.__last_open_folder is None: directory = os.path.expanduser("~") else: directory = self.__last_open_folder filter_ = settings.SUPPORTED_FILES.split(';;')[0] filename, _ = QFileDialog.getOpenFileName(self, self.tr("Open Database"), directory, filter_) # If is canceled, return if not filename: return # Remember the folder self.__last_open_folder = file_manager.get_path(filename) DEBUG("Abriendo la base de datos: '{}'".format(filename)) # If filename provide try: # Read pdb file pfile_object = pfile.File(filename) db_data = pfile_object.read() # Create a dict to manipulate data more easy db_data = self.__sanitize_data(db_data) except Exception as reason: QMessageBox.information(self, self.tr("The file couldn't be open"), str(reason)) CRITICAL("Error al intentar abrir el archivo: {}".format(reason)) return # Create a database container widget db_container = database_container.DatabaseContainer() try: db_container.create_database(db_data) except Exception as reason: QMessageBox.information(self, self.tr("Error"), str(reason)) CRITICAL("Error al crear la base de datos: {}".format(reason)) return # Set the PFile object to the new database db_container.pfile = pfile_object # Add data base container to stacked self.add_widget(db_container) # Database name db_name = file_manager.get_basename(filename) # Update title with the new database name, and enable some actions pireal = Pireal.get_service("pireal") pireal.change_title(db_name) pireal.set_enabled_db_actions(True) pireal.set_enabled_relation_actions(True) # Add to recent databases self.recent_databases = filename self.created = True def open_query(self): filter_ = settings.SUPPORTED_FILES.split(';;')[1] filename, _ = QFileDialog.getOpenFileName(self, self.tr("Open Query"), os.path.expanduser("~"), filter_) if not filename: return # FIXME: mejorar éste y new_query self.new_query(filename) def save_query(self, editor=None): db = self.get_active_db() fname = db.save_query(editor) if fname: self.querySaved.emit(self.tr("Query saved: {}".format(fname))) def save_query_as(self): pass def __sanitize_data(self, data): """ This function converts the data into a dictionary for better handling then. The argument 'data' is the content of the database. """ # FIXME: controlar cuando al final de la línea hay una coma data_dict = {'tables': []} for line_count, line in enumerate(data.splitlines()): # Ignore blank lines if not line: continue if line.startswith('@'): # This line is a header tpoint = line.find(':') if tpoint == -1: raise Exception("Invalid syntax at line {}".format( line_count + 1)) table_name, line = line.split(':') table_name = table_name[1:].strip() table_dict = {} table_dict['name'] = table_name table_dict['header'] = line.split(',') table_dict['tuples'] = [] else: for l in csv.reader([line]): # Remove spaces l = list(map(str.strip, l)) # FIXME: this is necesary? if table_dict['name'] == table_name: table_dict['tuples'].append(l) if not table_dict['tuples']: data_dict['tables'].append(table_dict) return data_dict def remove_last_widget(self): """ Remove last widget from stacked """ widget = self.stacked.widget(self.stacked.count() - 1) self.stacked.removeWidget(widget) def close_database(self): """ Close the database and return to the main widget """ db = self.get_active_db() query_container = db.query_container if db.modified: msgbox = QMessageBox(self) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowTitle(self.tr("Save Changes?")) msgbox.setText(self.tr("The <b>{}</b> database has ben" " modified.<br>Do you want save " "your changes?".format( db.dbname()))) cancel_btn = msgbox.addButton(self.tr("Cancel"), QMessageBox.RejectRole) msgbox.addButton(self.tr("No"), QMessageBox.NoRole) yes_btn = msgbox.addButton(self.tr("Yes"), QMessageBox.YesRole) msgbox.exec_() r = msgbox.clickedButton() if r == cancel_btn: return if r == yes_btn: self.save_database() # Check if editor is modified query_widget = query_container.currentWidget() if query_widget is not None: weditor = query_widget.get_editor() if weditor is not None: # TODO: duplicate code, see tab widget if weditor.modified: msgbox = QMessageBox(self) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowTitle(self.tr("File modified")) msgbox.setText(self.tr("The file <b>{}</b> has unsaved " "changes. You want to keep " "them?".format( weditor.name))) cancel_btn = msgbox.addButton(self.tr("Cancel"), QMessageBox.RejectRole) msgbox.addButton(self.tr("No"), QMessageBox.NoRole) yes_btn = msgbox.addButton(self.tr("Yes"), QMessageBox.YesRole) msgbox.exec_() r = msgbox.clickedButton() if r == cancel_btn: return if r == yes_btn: self.save_query(weditor) self.stacked.removeWidget(db) pireal = Pireal.get_service("pireal") pireal.set_enabled_db_actions(False) pireal.set_enabled_relation_actions(False) pireal.set_enabled_query_actions(False) pireal.set_enabled_editor_actions(False) self.created = False DEBUG("Se cerró la base de datos: '{}'".format(db.dbname())) del db def new_query(self, filename=''): pireal = Pireal.get_service("pireal") db_container = self.get_active_db() db_container.new_query(filename) # Enable editor actions # FIXME: refactoring pireal.set_enabled_query_actions(True) zoom_in_action = Pireal.get_action("zoom_in") zoom_in_action.setEnabled(True) zoom_out_action = Pireal.get_action("zoom_out") zoom_out_action.setEnabled(True) paste_action = Pireal.get_action("paste_action") paste_action.setEnabled(True) comment_action = Pireal.get_action("comment") comment_action.setEnabled(True) uncomment_action = Pireal.get_action("uncomment") uncomment_action.setEnabled(True) def execute_queries(self): db_container = self.get_active_db() db_container.execute_queries() def execute_selection(self): db_container = self.get_active_db() db_container.execute_selection() def save_database(self): db = self.get_active_db() if not db.modified: return # Get relations dict relations = db.table_widget.relations # Generate content content = file_manager.generate_database(relations) db.pfile.save(content=content) filename = db.pfile.filename # Emit signal self.databaseSaved.emit( self.tr("Database saved: {}".format(filename))) db.modified = False def save_database_as(self): filter = settings.SUPPORTED_FILES.split(';;')[0] filename, _ = QFileDialog.getSaveFileName(self, self.tr("Save Database As"), settings.PIREAL_PROJECTS, filter) if not filename: return db = self.get_active_db() # Get relations relations = db.table_widget.relations # Content content = file_manager.generate_database(relations) db.pfile.save(content, filename) self.databaseSaved.emit( self.tr("Database saved: {}".format(db.pfile.filename))) db.modified = False def remove_relation(self): db = self.get_active_db() if db.delete_relation(): db.modified = True def create_new_relation(self): data = new_relation_dialog.create_relation() if data is not None: db = self.get_active_db() rela, rela_name = data # Add table db.table_widget.add_table(rela, rela_name) # Add item to lateral widget db.lateral_widget.add_item(rela_name, rela.count()) # Set modified db db.modified = True def edit_relation(self): db = self.get_active_db() lateral = db.lateral_widget selected_items = lateral.selectedItems() if selected_items: selected_relation = selected_items[0].text(0) relation_text = selected_relation.split()[0].strip() rela = db.table_widget.relations[relation_text] data = edit_relation_dialog.edit_relation(rela) if data is not None: # Update table db.table_widget.update_table(data) # Update relation db.table_widget.relations[relation_text] = data # Set modified db db.modified = True lateral.update_item(data.count()) def load_relation(self, filename=''): """ Load Relation file """ if not filename: if self.__last_open_folder is None: directory = os.path.expanduser("~") else: directory = self.__last_open_folder msg = self.tr("Open Relation File") filter_ = settings.SUPPORTED_FILES.split(';;')[-1] filenames = QFileDialog.getOpenFileNames(self, msg, directory, filter_)[0] if not filenames: return # Save folder self.__last_open_folder = file_manager.get_path(filenames[0]) db_container = self.get_active_db() if db_container.load_relation(filenames): db_container.modified = True def add_start_page(self): """ This function adds the Start Page to the stacked widget """ sp = start_page.StartPage() self.add_widget(sp) def show_settings(self): """ Show settings dialog on stacked """ preferences_dialog = preferences.Preferences(self) if isinstance(self.widget(1), preferences.Preferences): self.widget(1).close() else: self.stacked.insertWidget(1, preferences_dialog) self.stacked.setCurrentIndex(1) # Connect the closed signal preferences_dialog.settingsClosed.connect(self._settings_closed) def widget(self, index): """ Returns the widget at the given index """ return self.stacked.widget(index) def add_widget(self, widget): """ Appends and show the given widget to the Stacked """ index = self.stacked.addWidget(widget) self.stacked.setCurrentIndex(index) def _settings_closed(self): self.stacked.removeWidget(self.widget(1)) self.stacked.setCurrentWidget(self.stacked.currentWidget()) def get_active_db(self): """ Return an instance of DatabaseContainer widget if the stacked contains an DatabaseContainer in last index or None if it's not an instance of DatabaseContainer """ index = self.stacked.count() - 1 widget = self.widget(index) if isinstance(widget, database_container.DatabaseContainer): return widget return None def get_unsaved_queries(self): query_container = self.get_active_db().query_container return query_container.get_unsaved_queries() def undo_action(self): query_container = self.get_active_db().query_container query_container.undo() def redo_action(self): query_container = self.get_active_db().query_container query_container.redo() def cut_action(self): query_container = self.get_active_db().query_container query_container.cut() def copy_action(self): query_container = self.get_active_db().query_container query_container.copy() def paste_action(self): query_container = self.get_active_db().query_container query_container.paste() def zoom_in(self): query_container = self.get_active_db().query_container query_container.zoom_in() def zoom_out(self): query_container = self.get_active_db().query_container query_container.zoom_out() def comment(self): query_container = self.get_active_db().query_container query_container.comment() def uncomment(self): query_container = self.get_active_db().query_container query_container.uncomment()
class TableWidget(QWidget): def __init__(self): super(TableWidget, self).__init__() vbox = QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0) self.relations = {} # Stack self.stacked = QStackedWidget() vbox.addWidget(self.stacked) def count(self): return self.stacked.count() def remove_table(self, index): widget = self.stacked.widget(index) self.stacked.removeWidget(widget) del widget def current_table(self): return self.stacked.currentWidget() def remove_relation(self, name): del self.relations[name] def add_relation(self, name, rela): if self.relations.get(name, None) is None: self.relations[name] = rela return True return False def update_table(self, data): current_table = self.current_table() model = current_table.model() # Clear content model.clear() # Add new header and content model.setHorizontalHeaderLabels(data.header) for row_count, row in enumerate(data.content): for col_count, data in enumerate(row): item = QStandardItem(data) item.setFlags(item.flags() & ~Qt.ItemIsEditable) # item.setSelectable(False) model.setItem(row_count, col_count, item) def add_table(self, rela, name): """ Add new table from New Relation Dialog """ # Create table table = self.create_table(rela) self.add_relation(name, rela) self.stacked.addWidget(table) def create_table(self, rela): table = custom_table.Table() model = QStandardItemModel() table.setModel(model) model.setHorizontalHeaderLabels(rela.header) for row_count, row in enumerate(rela.content): for col_count, data in enumerate(row): item = QStandardItem(data) item.setFlags(item.flags() & ~Qt.ItemIsEditable) model.setItem(row_count, col_count, item) return table
class QueryWidget(QWidget): editorModified = pyqtSignal(bool) def __init__(self): super(QueryWidget, self).__init__() box = QVBoxLayout(self) box.setContentsMargins(0, 0, 0, 0) self._vsplitter = QSplitter(Qt.Vertical) self._hsplitter = QSplitter(Qt.Horizontal) self._result_list = lateral_widget.LateralWidget() self._result_list.header().hide() self._hsplitter.addWidget(self._result_list) self._stack_tables = QStackedWidget() self._hsplitter.addWidget(self._stack_tables) self.relations = {} self._query_editor = editor.Editor() # Editor connections self._query_editor.customContextMenuRequested.connect( self.__show_context_menu) self._query_editor.modificationChanged[bool].connect( self.__editor_modified) self._query_editor.undoAvailable[bool].connect( self.__on_undo_available) self._query_editor.redoAvailable[bool].connect( self.__on_redo_available) self._query_editor.copyAvailable[bool].connect( self.__on_copy_available) self._vsplitter.addWidget(self._query_editor) self._vsplitter.addWidget(self._hsplitter) box.addWidget(self._vsplitter) # Connections self._result_list.itemClicked.connect( lambda index: self._stack_tables.setCurrentIndex( self._result_list.row())) self._result_list.itemDoubleClicked.connect( self.show_relation) def __show_context_menu(self, point): popup_menu = self._query_editor.createStandardContextMenu() undock_editor = QAction(self.tr("Undock"), self) popup_menu.insertAction(popup_menu.actions()[0], undock_editor) popup_menu.insertSeparator(popup_menu.actions()[1]) undock_editor.triggered.connect(self.__undock_editor) popup_menu.exec_(self.mapToGlobal(point)) def __undock_editor(self): new_editor = editor.Editor() actual_doc = self._query_editor.document() new_editor.setDocument(actual_doc) new_editor.resize(900, 400) # Set text cursor tc = self._query_editor.textCursor() new_editor.setTextCursor(tc) # Set title db = Pireal.get_service("central").get_active_db() qc = db.query_container new_editor.setWindowTitle(qc.tab_text(qc.current_index())) new_editor.show() def __on_undo_available(self, value): """ Change state of undo action """ pireal = Pireal.get_service("pireal") action = pireal.get_action("undo_action") action.setEnabled(value) def __on_redo_available(self, value): """ Change state of redo action """ pireal = Pireal.get_service("pireal") action = pireal.get_action("redo_action") action.setEnabled(value) def __on_copy_available(self, value): """ Change states of cut and copy action """ cut_action = Pireal.get_action("cut_action") cut_action.setEnabled(value) copy_action = Pireal.get_action("copy_action") copy_action.setEnabled(value) def show_relation(self, item): central_widget = Pireal.get_service("central") table_widget = central_widget.get_active_db().table_widget rela = self.relations[item.name] dialog = QDialog(self) dialog.resize(700, 500) dialog.setWindowTitle(item.name) box = QVBoxLayout(dialog) box.setContentsMargins(5, 5, 5, 5) table = table_widget.create_table(rela) box.addWidget(table) hbox = QHBoxLayout() btn = QPushButton(self.tr("Ok")) btn.clicked.connect(dialog.close) hbox.addStretch() hbox.addWidget(btn) box.addLayout(hbox) dialog.show() def save_sizes(self): """ Save sizes of Splitters """ qsettings = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat) qsettings.setValue('hsplitter_query_sizes', self._hsplitter.saveState()) qsettings.setValue('vsplitter_query_sizes', self._vsplitter.saveState()) def get_editor(self): return self._query_editor def __editor_modified(self, modified): self.editorModified.emit(modified) def showEvent(self, event): super(QueryWidget, self).showEvent(event) self._hsplitter.setSizes([1, self.width() / 3]) def clear_results(self): self._result_list.clear_items() i = self._stack_tables.count() while i >= 0: widget = self._stack_tables.widget(i) self._stack_tables.removeWidget(widget) if widget is not None: widget.deleteLater() i -= 1 def add_table(self, rela, rname): wtable = custom_table.Table() # Model model = QStandardItemModel() wtable.setModel(model) model.setHorizontalHeaderLabels(rela.header) for data in rela.content: nrow = model.rowCount() # wtable.insertRow(nrow) for col, text in enumerate(data): item = QStandardItem(text) item.setFlags(item.flags() & ~Qt.ItemIsEditable) model.setItem(nrow, col, item) index = self._stack_tables.addWidget(wtable) self._stack_tables.setCurrentIndex(index) self._result_list.add_item(rname, rela.count())
class TreeWindow(QMainWindow): """Class override for the main window. Contains main window views and controls. """ selectChanged = pyqtSignal() nodeModified = pyqtSignal(treenode.TreeNode) treeModified = pyqtSignal() winActivated = pyqtSignal(QMainWindow) winMinimized = pyqtSignal() winClosing = pyqtSignal(QMainWindow) def __init__(self, model, allActions, parent=None): """Initialize the main window. Arguments: model -- the initial data model allActions -- a dict containing the upper level actions parent -- the parent window, usually None """ super().__init__(parent) self.allActions = allActions.copy() self.allowCloseFlag = True self.winActions = {} self.toolbars = [] self.rightTabActList = [] self.setAttribute(Qt.WA_DeleteOnClose) self.setAcceptDrops(True) self.setStatusBar(QStatusBar()) self.setCaption() self.setupActions() self.setupMenus() self.setupToolbars() self.restoreToolbarPosition() self.treeView = treeview.TreeView(model, self.allActions) self.breadcrumbSplitter = QSplitter(Qt.Vertical) self.setCentralWidget(self.breadcrumbSplitter) self.breadcrumbView = breadcrumbview.BreadcrumbView(self.treeView) self.breadcrumbSplitter.addWidget(self.breadcrumbView) self.breadcrumbView.setVisible(globalref. genOptions['InitShowBreadcrumb']) self.treeSplitter = QSplitter() self.breadcrumbSplitter.addWidget(self.treeSplitter) self.treeStack = QStackedWidget() self.treeSplitter.addWidget(self.treeStack) self.treeStack.addWidget(self.treeView) self.treeView.shortcutEntered.connect(self.execShortcut) self.treeView.selectionModel().selectionChanged.connect(self. updateRightViews) self.treeFilterView = None self.rightTabs = QTabWidget() self.treeSplitter.addWidget(self.rightTabs) self.rightTabs.setTabPosition(QTabWidget.South) self.rightTabs.tabBar().setFocusPolicy(Qt.NoFocus) self.outputSplitter = QSplitter(Qt.Vertical) self.rightTabs.addTab(self.outputSplitter, _('Data Output')) parentOutputView = outputview.OutputView(self.treeView, False) parentOutputView.highlighted[str].connect(self.statusBar().showMessage) self.outputSplitter.addWidget(parentOutputView) childOutputView = outputview.OutputView(self.treeView, True) childOutputView.highlighted[str].connect(self.statusBar().showMessage) self.outputSplitter.addWidget(childOutputView) self.editorSplitter = QSplitter(Qt.Vertical) self.rightTabs.addTab(self.editorSplitter, _('Data Edit')) parentEditView = dataeditview.DataEditView(self.treeView, self.allActions, False) parentEditView.shortcutEntered.connect(self.execShortcut) parentEditView.focusOtherView.connect(self.focusNextView) parentEditView.inLinkSelectMode.connect(self.treeView. toggleNoMouseSelectMode) self.treeView.skippedMouseSelect.connect(parentEditView. internalLinkSelected) self.editorSplitter.addWidget(parentEditView) childEditView = dataeditview.DataEditView(self.treeView, self.allActions, True) childEditView.shortcutEntered.connect(self.execShortcut) childEditView.focusOtherView.connect(self.focusNextView) childEditView.inLinkSelectMode.connect(self.treeView. toggleNoMouseSelectMode) self.treeView.skippedMouseSelect.connect(childEditView. internalLinkSelected) parentEditView.hoverFocusActive.connect(childEditView.endEditor) childEditView.hoverFocusActive.connect(parentEditView.endEditor) parentEditView.inLinkSelectMode.connect(childEditView. updateInLinkSelectMode) childEditView.inLinkSelectMode.connect(parentEditView. updateInLinkSelectMode) self.editorSplitter.addWidget(childEditView) self.titleSplitter = QSplitter(Qt.Vertical) self.rightTabs.addTab(self.titleSplitter, _('Title List')) parentTitleView = titlelistview.TitleListView(self.treeView, False) parentTitleView.shortcutEntered.connect(self.execShortcut) self.titleSplitter.addWidget(parentTitleView) childTitleView = titlelistview.TitleListView(self.treeView, True) childTitleView.shortcutEntered.connect(self.execShortcut) self.titleSplitter.addWidget(childTitleView) self.rightTabs.currentChanged.connect(self.updateRightViews) self.updateFonts() def setExternalSignals(self): """Connect widow object signals to signals in this object. In a separate method to refresh after local control change. """ self.treeView.selectionModel().selectionChanged.connect(self. selectChanged) for i in range(2): self.editorSplitter.widget(i).nodeModified.connect(self. nodeModified) self.titleSplitter.widget(i).nodeModified.connect(self. nodeModified) self.titleSplitter.widget(i).treeModified.connect(self. treeModified) def updateActions(self, allActions): """Use new actions for menus, etc. when the local control changes. Arguments: allActions -- a dict containing the upper level actions """ # remove submenu actions that are children of the window self.removeAction(self.allActions['DataNodeType']) self.removeAction(self.allActions['FormatFontSize']) self.allActions = allActions.copy() self.allActions.update(self.winActions) self.menuBar().clear() self.setupMenus() self.addToolbarCommands() self.treeView.allActions = self.allActions for i in range(2): self.editorSplitter.widget(i).allActions = self.allActions def updateTreeNode(self, node): """Update all spots for the given node in the tree view. Arguments: node -- the node to be updated """ for spot in node.spotRefs: self.treeView.update(spot.index(self.treeView.model())) self.treeView.resizeColumnToContents(0) self.breadcrumbView.updateContents() def updateTree(self): """Update the full tree view. """ self.treeView.scheduleDelayedItemsLayout() self.breadcrumbView.updateContents() def updateRightViews(self, *args, outputOnly=False): """Update all right-hand views and breadcrumb view. Arguments: *args -- dummy arguments to collect args from signals outputOnly -- only update output views (not edit views) """ if globalref.mainControl.activeControl: self.rightTabActList[self.rightTabs. currentIndex()].setChecked(True) self.breadcrumbView.updateContents() splitter = self.rightTabs.currentWidget() if not outputOnly or isinstance(splitter.widget(0), outputview.OutputView): for i in range(2): splitter.widget(i).updateContents() def refreshDataEditViews(self): """Refresh the data in non-selected cells in curreent data edit views. """ splitter = self.rightTabs.currentWidget() if isinstance(splitter.widget(0), dataeditview.DataEditView): for i in range(2): splitter.widget(i).updateUnselectedCells() def updateCommandsAvail(self): """Set window commands available based on node selections. """ self.allActions['ViewPrevSelect'].setEnabled(len(self.treeView. selectionModel(). prevSpots) > 1) self.allActions['ViewNextSelect'].setEnabled(len(self.treeView. selectionModel(). nextSpots) > 0) def updateWinGenOptions(self): """Update tree and data edit windows based on general option changes. """ self.treeView.updateTreeGenOptions() for i in range(2): self.editorSplitter.widget(i).setMouseTracking(globalref. genOptions['EditorOnHover']) def updateFonts(self): """Update custom fonts in views. """ treeFont = QTextDocument().defaultFont() treeFontName = globalref.miscOptions['TreeFont'] if treeFontName: treeFont.fromString(treeFontName) self.treeView.setFont(treeFont) self.treeView.updateTreeGenOptions() if self.treeFilterView: self.treeFilterView.setFont(treeFont) ouputFont = QTextDocument().defaultFont() ouputFontName = globalref.miscOptions['OutputFont'] if ouputFontName: ouputFont.fromString(ouputFontName) editorFont = QTextDocument().defaultFont() editorFontName = globalref.miscOptions['EditorFont'] if editorFontName: editorFont.fromString(editorFontName) for i in range(2): self.outputSplitter.widget(i).setFont(ouputFont) self.editorSplitter.widget(i).setFont(editorFont) self.titleSplitter.widget(i).setFont(editorFont) def resetTreeModel(self, model): """Change the model assigned to the tree view. Arguments: model -- the new model to assign """ self.treeView.resetModel(model) self.treeView.selectionModel().selectionChanged.connect(self. updateRightViews) def activateAndRaise(self): """Activate this window and raise it to the front. """ self.activateWindow() self.raise_() def setCaption(self, pathObj=None, modified=False): """Change the window caption title based on the file name and path. Arguments: pathObj - a path object for the current file """ modFlag = '*' if modified else '' if pathObj: caption = '{0}{1} [{2}] - TreeLine'.format(str(pathObj.name), modFlag, str(pathObj.parent)) else: caption = '- TreeLine' self.setWindowTitle(caption) def filterView(self): """Create, show and return a filter view. """ self.removeFilterView() self.treeFilterView = treeview.TreeFilterView(self.treeView, self.allActions) self.treeFilterView.shortcutEntered.connect(self.execShortcut) self.treeView.selectionModel().selectionChanged.connect(self. treeFilterView. updateFromSelectionModel) for i in range(2): editView = self.editorSplitter.widget(i) editView.inLinkSelectMode.connect(self.treeFilterView. toggleNoMouseSelectMode) self.treeFilterView.skippedMouseSelect.connect(editView. internalLinkSelected) self.treeStack.addWidget(self.treeFilterView) self.treeStack.setCurrentWidget(self.treeFilterView) return self.treeFilterView def removeFilterView(self): """Hide and delete the current filter view. """ if self.treeFilterView != None: # check for None since False if empty self.treeStack.removeWidget(self.treeFilterView) globalref.mainControl.currentStatusBar().removeWidget(self. treeFilterView. messageLabel) self.treeFilterView.messageLabel.deleteLater() self.treeFilterView = None def rightParentView(self): """Return the current right-hand parent view if visible (or None). """ view = self.rightTabs.currentWidget().widget(0) if not view.isVisible() or view.height() == 0 or view.width() == 0: return None return view def rightChildView(self): """Return the current right-hand parent view if visible (or None). """ view = self.rightTabs.currentWidget().widget(1) if not view.isVisible() or view.height() == 0 or view.width() == 0: return None return view def focusNextView(self, forward=True): """Focus the next pane in the tab focus series. Called by a signal from the data edit views. Tab sequences tend to skip views without this. Arguments: forward -- forward in tab series if True """ reason = (Qt.TabFocusReason if forward else Qt.BacktabFocusReason) rightParent = self.rightParentView() rightChild = self.rightChildView() if (self.sender().isChildView == forward or (forward and rightChild == None) or (not forward and rightParent == None)): self.treeView.setFocus(reason) elif forward: rightChild.setFocus(reason) else: rightParent.setFocus(reason) def execShortcut(self, key): """Execute an action based on a shortcut key signal from a view. Arguments: key -- the QKeySequence shortcut """ keyDict = {action.shortcut().toString(): action for action in self.allActions.values()} try: action = keyDict[key.toString()] except KeyError: return if action.isEnabled(): action.trigger() def setupActions(self): """Add the actions for contols at the window level. These actions only affect an individual window, they're independent in multiple windows of the same file. """ viewExpandBranchAct = QAction(_('&Expand Full Branch'), self, statusTip=_('Expand all children of the selected nodes')) viewExpandBranchAct.triggered.connect(self.viewExpandBranch) self.winActions['ViewExpandBranch'] = viewExpandBranchAct viewCollapseBranchAct = QAction(_('&Collapse Full Branch'), self, statusTip=_('Collapse all children of the selected nodes')) viewCollapseBranchAct.triggered.connect(self.viewCollapseBranch) self.winActions['ViewCollapseBranch'] = viewCollapseBranchAct viewPrevSelectAct = QAction(_('&Previous Selection'), self, statusTip=_('Return to the previous tree selection')) viewPrevSelectAct.triggered.connect(self.viewPrevSelect) self.winActions['ViewPrevSelect'] = viewPrevSelectAct viewNextSelectAct = QAction(_('&Next Selection'), self, statusTip=_('Go to the next tree selection in history')) viewNextSelectAct.triggered.connect(self.viewNextSelect) self.winActions['ViewNextSelect'] = viewNextSelectAct viewRightTabGrp = QActionGroup(self) viewOutputAct = QAction(_('Show Data &Output'), viewRightTabGrp, statusTip=_('Show data output in right view'), checkable=True) self.winActions['ViewDataOutput'] = viewOutputAct viewEditAct = QAction(_('Show Data &Editor'), viewRightTabGrp, statusTip=_('Show data editor in right view'), checkable=True) self.winActions['ViewDataEditor'] = viewEditAct viewTitleAct = QAction(_('Show &Title List'), viewRightTabGrp, statusTip=_('Show title list in right view'), checkable=True) self.winActions['ViewTitleList'] = viewTitleAct self.rightTabActList = [viewOutputAct, viewEditAct, viewTitleAct] viewRightTabGrp.triggered.connect(self.viewRightTab) viewBreadcrumbAct = QAction(_('Show &Breadcrumb View'), self, statusTip=_('Toggle showing breadcrumb ancestor view'), checkable=True) viewBreadcrumbAct.setChecked(globalref. genOptions['InitShowBreadcrumb']) viewBreadcrumbAct.triggered.connect(self.viewBreadcrumb) self.winActions['ViewBreadcrumb'] = viewBreadcrumbAct viewChildPaneAct = QAction(_('&Show Child Pane'), self, statusTip=_('Toggle showing right-hand child views'), checkable=True) viewChildPaneAct.setChecked(globalref.genOptions['InitShowChildPane']) viewChildPaneAct.triggered.connect(self.viewShowChildPane) self.winActions['ViewShowChildPane'] = viewChildPaneAct viewDescendAct = QAction(_('Show Output &Descendants'), self, statusTip=_('Toggle showing output view indented descendants'), checkable=True) viewDescendAct.setChecked(globalref.genOptions['InitShowDescendants']) viewDescendAct.triggered.connect(self.viewDescendants) self.winActions['ViewShowDescend'] = viewDescendAct winCloseAct = QAction(_('&Close Window'), self, statusTip=_('Close this window')) winCloseAct.triggered.connect(self.close) self.winActions['WinCloseWindow'] = winCloseAct incremSearchStartAct = QAction(_('Start Incremental Search'), self) incremSearchStartAct.triggered.connect(self.incremSearchStart) self.addAction(incremSearchStartAct) self.winActions['IncremSearchStart'] = incremSearchStartAct incremSearchNextAct = QAction(_('Next Incremental Search'), self) incremSearchNextAct.triggered.connect(self.incremSearchNext) self.addAction(incremSearchNextAct) self.winActions['IncremSearchNext'] = incremSearchNextAct incremSearchPrevAct = QAction(_('Previous Incremental Search'), self) incremSearchPrevAct.triggered.connect(self.incremSearchPrev) self.addAction(incremSearchPrevAct) self.winActions['IncremSearchPrev'] = incremSearchPrevAct for name, action in self.winActions.items(): icon = globalref.toolIcons.getIcon(name.lower()) if icon: action.setIcon(icon) key = globalref.keyboardOptions[name] if not key.isEmpty(): action.setShortcut(key) self.allActions.update(self.winActions) def setupToolbars(self): """Add toolbars based on option settings. """ for toolbar in self.toolbars: self.removeToolBar(toolbar) self.toolbars = [] numToolbars = globalref.toolbarOptions['ToolbarQuantity'] iconSize = globalref.toolbarOptions['ToolbarSize'] for num in range(numToolbars): name = 'Toolbar{:d}'.format(num) toolbar = self.addToolBar(name) toolbar.setObjectName(name) toolbar.setIconSize(QSize(iconSize, iconSize)) self.toolbars.append(toolbar) self.addToolbarCommands() def addToolbarCommands(self): """Add toolbar commands for current actions. """ for toolbar, commandList in zip(self.toolbars, globalref. toolbarOptions['ToolbarCommands']): toolbar.clear() for command in commandList.split(','): if command: try: toolbar.addAction(self.allActions[command]) except KeyError: pass else: toolbar.addSeparator() def setupMenus(self): """Add menu items for actions. """ self.fileMenu = self.menuBar().addMenu(_('&File')) self.fileMenu.aboutToShow.connect(self.loadRecentMenu) self.fileMenu.addAction(self.allActions['FileNew']) self.fileMenu.addAction(self.allActions['FileOpen']) self.fileMenu.addAction(self.allActions['FileOpenSample']) self.fileMenu.addAction(self.allActions['FileImport']) self.fileMenu.addSeparator() self.fileMenu.addAction(self.allActions['FileSave']) self.fileMenu.addAction(self.allActions['FileSaveAs']) self.fileMenu.addAction(self.allActions['FileExport']) self.fileMenu.addAction(self.allActions['FileProperties']) self.fileMenu.addSeparator() self.fileMenu.addAction(self.allActions['FilePrintSetup']) self.fileMenu.addAction(self.allActions['FilePrintPreview']) self.fileMenu.addAction(self.allActions['FilePrint']) self.fileMenu.addAction(self.allActions['FilePrintPdf']) self.fileMenu.addSeparator() self.recentFileSep = self.fileMenu.addSeparator() self.fileMenu.addAction(self.allActions['FileQuit']) editMenu = self.menuBar().addMenu(_('&Edit')) editMenu.addAction(self.allActions['EditUndo']) editMenu.addAction(self.allActions['EditRedo']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditCut']) editMenu.addAction(self.allActions['EditCopy']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditPaste']) editMenu.addAction(self.allActions['EditPastePlain']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditPasteChild']) editMenu.addAction(self.allActions['EditPasteBefore']) editMenu.addAction(self.allActions['EditPasteAfter']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditPasteCloneChild']) editMenu.addAction(self.allActions['EditPasteCloneBefore']) editMenu.addAction(self.allActions['EditPasteCloneAfter']) nodeMenu = self.menuBar().addMenu(_('&Node')) nodeMenu.addAction(self.allActions['NodeRename']) nodeMenu.addSeparator() nodeMenu.addAction(self.allActions['NodeAddChild']) nodeMenu.addAction(self.allActions['NodeInsertBefore']) nodeMenu.addAction(self.allActions['NodeInsertAfter']) nodeMenu.addSeparator() nodeMenu.addAction(self.allActions['NodeDelete']) nodeMenu.addAction(self.allActions['NodeIndent']) nodeMenu.addAction(self.allActions['NodeUnindent']) nodeMenu.addSeparator() nodeMenu.addAction(self.allActions['NodeMoveUp']) nodeMenu.addAction(self.allActions['NodeMoveDown']) nodeMenu.addAction(self.allActions['NodeMoveFirst']) nodeMenu.addAction(self.allActions['NodeMoveLast']) dataMenu = self.menuBar().addMenu(_('&Data')) # add action's parent to get the sub-menu dataMenu.addMenu(self.allActions['DataNodeType'].parent()) # add the action to activate the shortcut key self.addAction(self.allActions['DataNodeType']) dataMenu.addAction(self.allActions['DataConfigType']) dataMenu.addAction(self.allActions['DataCopyType']) dataMenu.addAction(self.allActions['DataVisualConfig']) dataMenu.addSeparator() dataMenu.addAction(self.allActions['DataSortNodes']) dataMenu.addAction(self.allActions['DataNumbering']) dataMenu.addAction(self.allActions['DataRegenRefs']) dataMenu.addSeparator() dataMenu.addAction(self.allActions['DataCloneMatches']) dataMenu.addAction(self.allActions['DataDetachClones']) dataMenu.addSeparator() dataMenu.addAction(self.allActions['DataFlatCategory']) dataMenu.addAction(self.allActions['DataAddCategory']) dataMenu.addAction(self.allActions['DataSwapCategory']) toolsMenu = self.menuBar().addMenu(_('&Tools')) toolsMenu.addAction(self.allActions['ToolsFindText']) toolsMenu.addAction(self.allActions['ToolsFindCondition']) toolsMenu.addAction(self.allActions['ToolsFindReplace']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsFilterText']) toolsMenu.addAction(self.allActions['ToolsFilterCondition']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsSpellCheck']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsGenOptions']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsShortcuts']) toolsMenu.addAction(self.allActions['ToolsToolbars']) toolsMenu.addAction(self.allActions['ToolsFonts']) toolsMenu.addAction(self.allActions['ToolsColors']) formatMenu = self.menuBar().addMenu(_('Fo&rmat')) formatMenu.addAction(self.allActions['FormatBoldFont']) formatMenu.addAction(self.allActions['FormatItalicFont']) formatMenu.addAction(self.allActions['FormatUnderlineFont']) formatMenu.addSeparator() # add action's parent to get the sub-menu formatMenu.addMenu(self.allActions['FormatFontSize'].parent()) # add the action to activate the shortcut key self.addAction(self.allActions['FormatFontSize']) formatMenu.addAction(self.allActions['FormatFontColor']) formatMenu.addSeparator() formatMenu.addAction(self.allActions['FormatExtLink']) formatMenu.addAction(self.allActions['FormatIntLink']) formatMenu.addSeparator() formatMenu.addAction(self.allActions['FormatSelectAll']) formatMenu.addAction(self.allActions['FormatClearFormat']) viewMenu = self.menuBar().addMenu(_('&View')) viewMenu.addAction(self.allActions['ViewExpandBranch']) viewMenu.addAction(self.allActions['ViewCollapseBranch']) viewMenu.addSeparator() viewMenu.addAction(self.allActions['ViewPrevSelect']) viewMenu.addAction(self.allActions['ViewNextSelect']) viewMenu.addSeparator() viewMenu.addAction(self.allActions['ViewDataOutput']) viewMenu.addAction(self.allActions['ViewDataEditor']) viewMenu.addAction(self.allActions['ViewTitleList']) viewMenu.addSeparator() viewMenu.addAction(self.allActions['ViewBreadcrumb']) viewMenu.addAction(self.allActions['ViewShowChildPane']) viewMenu.addAction(self.allActions['ViewShowDescend']) self.windowMenu = self.menuBar().addMenu(_('&Window')) self.windowMenu.aboutToShow.connect(self.loadWindowMenu) self.windowMenu.addAction(self.allActions['WinNewWindow']) self.windowMenu.addAction(self.allActions['WinCloseWindow']) self.windowMenu.addSeparator() helpMenu = self.menuBar().addMenu(_('&Help')) helpMenu.addAction(self.allActions['HelpBasic']) helpMenu.addAction(self.allActions['HelpFull']) helpMenu.addSeparator() helpMenu.addAction(self.allActions['HelpAbout']) def viewExpandBranch(self): """Expand all children of the selected spots. """ QApplication.setOverrideCursor(Qt.WaitCursor) selectedSpots = self.treeView.selectionModel().selectedSpots() if not selectedSpots: selectedSpots = self.treeView.model().treeStructure.rootSpots() for spot in selectedSpots: self.treeView.expandBranch(spot) QApplication.restoreOverrideCursor() def viewCollapseBranch(self): """Collapse all children of the selected spots. """ QApplication.setOverrideCursor(Qt.WaitCursor) selectedSpots = self.treeView.selectionModel().selectedSpots() if not selectedSpots: selectedSpots = self.treeView.model().treeStructure.rootSpots() for spot in selectedSpots: self.treeView.collapseBranch(spot) QApplication.restoreOverrideCursor() def viewPrevSelect(self): """Return to the previous tree selection. """ self.treeView.selectionModel().restorePrevSelect() def viewNextSelect(self): """Go to the next tree selection in history. """ self.treeView.selectionModel().restoreNextSelect() def viewRightTab(self, action): """Show the tab in the right-hand view given by action. Arguments: action -- the action triggered in the action group """ if action == self.allActions['ViewDataOutput']: self.rightTabs.setCurrentWidget(self.outputSplitter) elif action == self.allActions['ViewDataEditor']: self.rightTabs.setCurrentWidget(self.editorSplitter) else: self.rightTabs.setCurrentWidget(self.titleSplitter) def viewBreadcrumb(self, checked): """Enable or disable the display of the breadcrumb view. Arguments: checked -- True if to be shown, False if to be hidden """ self.breadcrumbView.setVisible(checked) if checked: self.updateRightViews() def viewShowChildPane(self, checked): """Enable or disable the display of children in a split pane. Arguments: checked -- True if to be shown, False if to be hidden """ for tabNum in range(3): for splitNum in range(2): view = self.rightTabs.widget(tabNum).widget(splitNum) view.hideChildView = not checked self.updateRightViews() def viewDescendants(self, checked): """Set the output view to show indented descendants if checked. Arguments: checked -- True if to be shown, False if to be hidden """ self.outputSplitter.widget(1).showDescendants = checked self.updateRightViews() def incremSearchStart(self): """Start an incremental title search. """ if not self.treeFilterView: self.treeView.setFocus() self.treeView.incremSearchStart() def incremSearchNext(self): """Go to the next match in an incremental title search. """ if not self.treeFilterView: self.treeView.incremSearchNext() def incremSearchPrev(self): """Go to the previous match in an incremental title search. """ if not self.treeFilterView: self.treeView.incremSearchPrev() def loadRecentMenu(self): """Load recent file items to file menu before showing. """ for action in self.fileMenu.actions(): text = action.text() if len(text) > 1 and text[0] == '&' and '0' <= text[1] <= '9': self.fileMenu.removeAction(action) self.fileMenu.insertActions(self.recentFileSep, globalref.mainControl.recentFiles. getActions()) def loadWindowMenu(self): """Load window list items to window menu before showing. """ for action in self.windowMenu.actions(): text = action.text() if len(text) > 1 and text[0] == '&' and '0' <= text[1] <= '9': self.windowMenu.removeAction(action) self.windowMenu.addActions(globalref.mainControl.windowActions()) def saveWindowGeom(self): """Save window geometry parameters to history options. """ contentsRect = self.geometry() frameRect = self.frameGeometry() globalref.histOptions.changeValue('WindowXSize', contentsRect.width()) globalref.histOptions.changeValue('WindowYSize', contentsRect.height()) globalref.histOptions.changeValue('WindowXPos', contentsRect.x()) globalref.histOptions.changeValue('WindowYPos', contentsRect.y()) globalref.histOptions.changeValue('WindowTopMargin', contentsRect.y() - frameRect.y()) globalref.histOptions.changeValue('WindowOtherMargin', contentsRect.x() - frameRect.x()) try: upperWidth, lowerWidth = self.breadcrumbSplitter.sizes() crumbPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('CrumbSplitPercent', crumbPercent) leftWidth, rightWidth = self.treeSplitter.sizes() treePercent = int(100 * leftWidth / (leftWidth + rightWidth)) globalref.histOptions.changeValue('TreeSplitPercent', treePercent) upperWidth, lowerWidth = self.outputSplitter.sizes() outputPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('OutputSplitPercent', outputPercent) upperWidth, lowerWidth = self.editorSplitter.sizes() editorPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('EditorSplitPercent', editorPercent) upperWidth, lowerWidth = self.titleSplitter.sizes() titlePercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('TitleSplitPercent', titlePercent) except ZeroDivisionError: pass # skip if splitter sizes were never set tabNum = self.rightTabs.currentIndex() globalref.histOptions.changeValue('ActiveRightView', tabNum) def restoreWindowGeom(self, offset=0): """Restore window geometry from history options. Arguments: offset -- number of pixels to offset window, down and to right """ rect = QRect(globalref.histOptions['WindowXPos'], globalref.histOptions['WindowYPos'], globalref.histOptions['WindowXSize'], globalref.histOptions['WindowYSize']) if rect.x() == -1000 and rect.y() == -1000: # let OS position window the first time self.resize(rect.size()) else: if offset: rect.adjust(offset, offset, offset, offset) availRect = QApplication.primaryScreen().availableVirtualGeometry() topMargin = globalref.histOptions['WindowTopMargin'] otherMargin = globalref.histOptions['WindowOtherMargin'] # remove frame space from available rect availRect.adjust(otherMargin, topMargin, -otherMargin, -otherMargin) finalRect = rect.intersected(availRect) if finalRect.isEmpty(): rect.moveTo(0, 0) finalRect = rect.intersected(availRect) if finalRect.isValid(): self.setGeometry(finalRect) crumbWidth = int(self.breadcrumbSplitter.width() / 100 * globalref.histOptions['CrumbSplitPercent']) self.breadcrumbSplitter.setSizes([crumbWidth, self.breadcrumbSplitter.width() - crumbWidth]) treeWidth = int(self.treeSplitter.width() / 100 * globalref.histOptions['TreeSplitPercent']) self.treeSplitter.setSizes([treeWidth, self.treeSplitter.width() - treeWidth]) outHeight = int(self.outputSplitter.height() / 100.0 * globalref.histOptions['OutputSplitPercent']) self.outputSplitter.setSizes([outHeight, self.outputSplitter.height() - outHeight]) editHeight = int(self.editorSplitter.height() / 100.0 * globalref.histOptions['EditorSplitPercent']) self.editorSplitter.setSizes([editHeight, self.editorSplitter.height() - editHeight]) titleHeight = int(self.titleSplitter.height() / 100.0 * globalref.histOptions['TitleSplitPercent']) self.titleSplitter.setSizes([titleHeight, self.titleSplitter.height() - titleHeight]) self.rightTabs.setCurrentIndex(globalref. histOptions['ActiveRightView']) def resetWindowGeom(self): """Set all stored window geometry values back to default settings. """ globalref.histOptions.resetToDefaults(['WindowXPos', 'WindowYPos', 'WindowXSize', 'WindowYSize', 'CrumbSplitPercent', 'TreeSplitPercent', 'OutputSplitPercent', 'EditorSplitPercent', 'TitleSplitPercent', 'ActiveRightView']) def saveToolbarPosition(self): """Save the toolbar position to the toolbar options. """ toolbarPos = base64.b64encode(self.saveState().data()).decode('ascii') globalref.toolbarOptions.changeValue('ToolbarPosition', toolbarPos) globalref.toolbarOptions.writeFile() def restoreToolbarPosition(self): """Restore the toolbar position from the toolbar options. """ toolbarPos = globalref.toolbarOptions['ToolbarPosition'] if toolbarPos: self.restoreState(base64.b64decode(bytes(toolbarPos, 'ascii'))) def dragEnterEvent(self, event): """Accept drags of files to this window. Arguments: event -- the drag event object """ if event.mimeData().hasUrls(): event.accept() def dropEvent(self, event): """Open a file dropped onto this window. Arguments: event -- the drop event object """ fileList = event.mimeData().urls() if fileList: path = pathlib.Path(fileList[0].toLocalFile()) globalref.mainControl.openFile(path, checkModified=True) def changeEvent(self, event): """Detect an activation of the main window and emit a signal. Arguments: event -- the change event object """ super().changeEvent(event) if (event.type() == QEvent.ActivationChange and QApplication.activeWindow() == self): self.winActivated.emit(self) elif (event.type() == QEvent.WindowStateChange and globalref.genOptions['MinToSysTray'] and self.isMinimized()): self.winMinimized.emit() def closeEvent(self, event): """Signal that the view is closing and close if the flag allows it. Also save window status if necessary. Arguments: event -- the close event object """ self.winClosing.emit(self) if self.allowCloseFlag: event.accept() else: event.ignore()
class MainWindow(QMainWindow): def __init__(self, uci_page): super().__init__() self.uci_page = uci_page self.add_menu_bar() self.create_content_table() self.window_init() self.change_central_widget_event() self.dataset_data = pd.DataFrame() def window_init(self): self.setWindowTitle(APP_TITLE) self.show() def create_content_table(self): self.content_table = ContentTable(self, self.uci_page) self.central_widget = QStackedWidget() self.central_widget.addWidget(self.content_table) self.central_widget.setCurrentWidget(self.content_table) self.setCentralWidget(self.central_widget) def add_menu_bar(self): self.qmenubar = QMenuBar(self) self.add_file_bar_menus() self.add_visualizer_bar_menus() self.add_normilization_bar_menus() self.setMenuBar(self.qmenubar) def add_file_bar_menus(self): file_menu = self.qmenubar.addMenu('&File') file_menu.addAction(self.open_dataset_action()) file_menu.addAction(self.attr_choose_action()) file_menu.addAction(self.show_pandas_action()) file_menu.addAction(self.quit_app_action()) def add_visualizer_bar_menus(self): file_menu = self.qmenubar.addMenu('&Visualizer') file_menu.addAction(self.histogram_plot_action()) file_menu.addAction(self.andrew_plot_action()) file_menu.addAction(self.parallel_plot_action()) file_menu.addAction(self.radviz_plot_action()) file_menu.addAction(self.heatmap_plot_action()) file_menu.addAction(self.scatter_matrix_plot_action()) file_menu.addAction(self.pca_plot_action()) file_menu.addAction(self.lda_plot_action()) def add_normilization_bar_menus(self): file_menu = self.qmenubar.addMenu('&Normilization') file_menu.addAction(self.norm_by_max_action()) file_menu.addAction(self.norm_by_min_action()) file_menu.addAction(self.norm_by_mean_action()) file_menu.addAction(self.norm_by_minimax_action()) file_menu.addAction(self.mean_norm_action()) def set_dataset_file(self, filename): self.dataset_file = filename print(self.dataset_file) def handle_open_dataset_action(self): filename = import_dataset_action() column_file = os.path.dirname(filename) + "/column_names.txt" print(column_file) self.set_dataset_file(filename) if not file_is_exists(column_file): information_dialod(NOT_SUCCESS_IMPORT_MESSAGE) else: self.dataset_cols = ColumnImporter(column_file).get_columns() dataset_importer = DatasetImporter(self.dataset_file, self.dataset_cols) self.dataset_data = dataset_importer.get_dataset() self.bak_data = self.dataset_data if info_import_dataset_dialog(SUCCESS_IMPORT_MESSAGE): self.show_pandas_table() def handle_attr_choose_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: cols = Chooser(self.dataset_cols[1:], self).get_data() self.dataset_data = self.dataset_data[cols] def show_pandas_table(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: pd_table = QTableView() model = PandasModel(self.dataset_data) pd_table.setModel(model) self.central_widget.addWidget(pd_table) self.central_widget.setCurrentWidget(pd_table) def handle_hist_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_histogram(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_andrew_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_andrew_curves(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_parallel_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_parallel_coordinates(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_radviz_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_radviz(self.dataset_data) cur_widget = self.central_widget.currentWidget() self.dataset_data = self.bak_data if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_heatmap_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_heatmap(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_scatter_matrix_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_scatter_matrix(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_pca_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_pca(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_lda_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_lda(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def show_pandas_action(self): action = QAction('Show imported dataset', self) action.setShortcut('Ctrl+s') action.triggered.connect(self.show_pandas_table) return action def attr_choose_action(self): action = QAction('Choose Attributes', self) action.triggered.connect(self.handle_attr_choose_action) return action def quit_app_action(self): action = QAction('Exit', self) action.setShortcut('Ctrl+q') action.setStatusTip('Exit from visualizer') action.triggered.connect(self.close) return action def open_dataset_action(self): action = QAction('Import dataset', self) action.setShortcut('Ctrl+I') action.setStatusTip('Import dataset from folder with it') action.triggered.connect( lambda state: self.handle_open_dataset_action()) return action def histogram_plot_action(self): action = QAction('Plot Histogram', self) action.triggered.connect(self.handle_hist_plot_action) return action def andrew_plot_action(self): action = QAction("Plot Andrew's Curves", self) action.triggered.connect(self.handle_andrew_plot_action) return action def parallel_plot_action(self): action = QAction("Plot Parallel Coordinates", self) action.triggered.connect(self.handle_parallel_plot_action) return action def radviz_plot_action(self): action = QAction("Plot Radviz", self) action.triggered.connect(self.handle_radviz_plot_action) return action def heatmap_plot_action(self): action = QAction("Plot Heatmap", self) action.triggered.connect(self.handle_heatmap_plot_action) return action def scatter_matrix_plot_action(self): action = QAction("Plot Scatter Matrix", self) action.triggered.connect(self.handle_scatter_matrix_plot_action) return action def pca_plot_action(self): action = QAction("Plot PCA", self) action.triggered.connect(self.handle_pca_plot_action) return action def lda_plot_action(self): action = QAction("Plot LDA", self) action.triggered.connect(self.handle_lda_plot_action) return action def norm_by_max_action(self): action = QAction('Normilize data by max value', self) action.triggered.connect(self.handle_norm_by_max_action) return action def norm_by_min_action(self): action = QAction('Normilize data by min value', self) action.triggered.connect(self.handle_norm_by_min_action) return action def norm_by_mean_action(self): action = QAction('Normilize data by mean value', self) action.triggered.connect(self.handle_norm_by_mean_action) return action def norm_by_minimax_action(self): action = QAction('Normilize data by minimax method', self) action.triggered.connect(self.handle_norm_by_minimax_action) return action def mean_norm_action(self): action = QAction('Normilize data by mean method', self) action.triggered.connect(self.handle_norm_mean_action) return action def change_central_widget_event(self): shortcut = QShortcut(QKeySequence("Ctrl+n"), self) shortcut.activated.connect(self.handle_central_widget) def handle_norm_by_max_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.normilize_by_max_value() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("by max value")) def handle_norm_by_min_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.normilize_by_min_value() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("by min value")) def handle_norm_by_mean_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.normilize_by_mean_value() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("by mean value")) def handle_norm_by_minimax_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.normilize_by_minimax() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("minimax method")) def handle_norm_mean_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.mean_normilization() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("mean method")) def handle_central_widget(self): cur_widget = self.central_widget.currentWidget() self.central_widget.removeWidget(cur_widget) self.central_widget.setCurrentWidget(self.content_table)
class SForm(QWidget): finishSignal = pyqtSignal() # 结束线程的信号 def __init__(self): super().__init__() self.userBox = {} # 每个用户对话框的字典 self.server = None self.initUI() # 绘制界面 def initUI(self): self.createGridGroupBox() self.creatVboxGroupBox() self.creatFormGroupBox() mainLayout = QVBoxLayout() hboxLayout = QHBoxLayout() hboxLayout.addStretch() # 调用方法绘制界面 self.setWindowTitle('服务器软件') hboxLayout.addWidget(self.gridGroupBox) hboxLayout.addWidget(self.vboxGroupBox) mainLayout.addLayout(hboxLayout) mainLayout.addWidget(self.formGroupBox) self.setLayout(mainLayout) # 绘制服务器配置部分 def createGridGroupBox(self): self.gridGroupBox = QGroupBox('服务器配置') layout = QGridLayout() # 设置标签、输入框 iplb = QLabel('服务器地址') self.ip = QLineEdit('localhost') self.ip.setEnabled(False) portlb = QLabel('开放端口') self.port = QLineEdit('1234') self.port.setEnabled(False) maxlb = QLabel('最大连接数') self.maxnum = QLineEdit('5') self.flb = QLabel('工作文件夹') self.fpath = QLineEdit('./sfile') self.selpath = QPushButton('选择') # 为按钮绑定点击事件 self.selpath.clicked.connect(self.showDialog) self.runbt = QPushButton('启动') self.runbt.clicked.connect(self.startServer) # 为启动按钮绑定服务器启动方法 layout.setSpacing(10) layout.addWidget(iplb, 1, 0) layout.addWidget(self.ip, 1, 1) layout.addWidget(portlb, 2, 0) layout.addWidget(self.port, 2, 1) layout.addWidget(maxlb, 3, 0) layout.addWidget(self.maxnum, 3, 1) layout.addWidget(self.flb, 4, 0) layout.addWidget(self.fpath, 4, 1) layout.addWidget(self.selpath, 5, 0) layout.addWidget(self.runbt, 5, 1) layout.setColumnStretch(1, 10) self.gridGroupBox.setLayout(layout) # 绘制服务日志部分 def creatVboxGroupBox(self): self.vboxGroupBox = QGroupBox('服务日志') layout = QVBoxLayout() self.log = QTextBrowser() layout.addWidget(self.log) self.vboxGroupBox.setLayout(layout) # 绘制消息对话框 def creatFormGroupBox(self): self.formGroupBox = QGroupBox('消息') layout = QFormLayout() msgbox = QTextBrowser() self.stack = QStackedWidget(self) # 设置一个堆栈以切换不同用户的对话界面 self.stack.addWidget(msgbox) # 每个用户有一个文本框展示信息 self.userBox['无'] = msgbox self.showMsg('无', info) self.selur = QComboBox() # 使用下拉列表选择用户对话框 self.selur.addItem('无') self.selur.currentTextChanged.connect(self.changeBox) # 绑定处理方法 self.selur.setDisabled(True) # 绘制输入框和发送按钮 childgrid = QGridLayout() self.umsg = QLineEdit() self.sendbt = QPushButton('发送') childgrid.addWidget(self.umsg, 0, 0) childgrid.addWidget(self.sendbt, 0, 1) layout.addRow(self.stack) layout.addRow(self.selur, childgrid) self.sendbt.clicked.connect(self.sendMsg) # 一开始禁用输入框和发送按钮 self.umsg.setEnabled(False) self.sendbt.setEnabled(False) self.formGroupBox.setLayout(layout) # 展示选择文件夹对话框 def showDialog(self): upath = QFileDialog.getExistingDirectory(self, '选择文件夹', '.') if not upath: upath = './sfile' self.fpath.setText(upath) # 开始服务器线程 def startServer(self): host = self.ip.text() port = int(self.port.text()) num = int(self.maxnum.text()) path = self.fpath.text() # 检测要求输入的字段是否为空 if host and port and num and path: self.selur.setEnabled(True) self.runbt.setEnabled(False) self.selpath.setEnabled(False) # 实例化服务器线程 self.sthread = QThread() self.server = Server(host, port, num, path) # 绑定信号与槽 self.server.statSignal.connect(self.addLog) self.server.lgSignal.connect(self.addUser) self.server.msgSignal.connect(self.showMsg) self.server.quitSignal.connect(self.removeUser) # 启动线程运行 self.server.moveToThread(self.sthread) self.sthread.started.connect(self.server.run) self.sthread.start() else: QMessageBox.information(self, '警告', '配置项不能为空!') # 发出警告 # 更新服务器日志 def addLog(self, logmsg): self.log.append(logmsg) # 为选择用户下拉列表中添加用户 def addUser(self, ur): self.selur.addItem(ur) umsgBox = QTextBrowser() self.userBox[ur] = umsgBox self.stack.addWidget(umsgBox) # 显示信息 def showMsg(self, ur, msg): self.userBox[ur].append(msg) # 移除用户 def removeUser(self, ur): i = self.selur.findText(ur) self.selur.removeItem(i) self.stack.removeWidget(self.userBox[ur]) # 根据选择的用户改变当前对话框 def changeBox(self, ur): if ur != '无': self.umsg.setEnabled(True) self.sendbt.setEnabled(True) self.stack.setCurrentWidget(self.userBox[ur]) # 发送消息 def sendMsg(self): msg = self.umsg.text() now = time.strftime('%H:%M:%S') umsg = '本机(' + now + '): ' + msg self.stack.currentWidget().append(umsg) # 在对话框中展示消息 self.umsg.clear() # 清空输入框 data = {'type': 'msg', 'cnt': {'msg': msg}} # 构造命令 ur = self.selur.currentText() self.server.users[ur].put(data) # 发送数据 # 自定义关闭事件 def closeEvent(self, event): if self.server: users = self.server.users for ur in users: data = {'type': 'end'} data['ur'] = ur users[ur].put(data) self.close()
class E5SideBar(QWidget): """ Class implementing a sidebar with a widget area, that is hidden or shown, if the current tab is clicked again. """ Version = 1 North = 0 East = 1 South = 2 West = 3 def __init__(self, orientation=None, delay=200, parent=None): """ Constructor @param orientation orientation of the sidebar widget (North, East, South, West) @param delay value for the expand/shrink delay in milliseconds (integer) @param parent parent widget (QWidget) """ super(E5SideBar, self).__init__(parent) self.__tabBar = QTabBar() self.__tabBar.setDrawBase(True) self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setUsesScrollButtons(True) self.__tabBar.setDrawBase(False) self.__stackedWidget = QStackedWidget(self) self.__stackedWidget.setContentsMargins(0, 0, 0, 0) self.__autoHideButton = QToolButton() self.__autoHideButton.setCheckable(True) self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) self.__autoHideButton.setChecked(True) self.__autoHideButton.setToolTip( self.tr("Deselect to activate automatic collapsing")) self.barLayout = QBoxLayout(QBoxLayout.LeftToRight) self.barLayout.setContentsMargins(0, 0, 0, 0) self.layout = QBoxLayout(QBoxLayout.TopToBottom) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.barLayout.addWidget(self.__autoHideButton) self.barLayout.addWidget(self.__tabBar) self.layout.addLayout(self.barLayout) self.layout.addWidget(self.__stackedWidget) self.setLayout(self.layout) # initialize the delay timer self.__actionMethod = None self.__delayTimer = QTimer(self) self.__delayTimer.setSingleShot(True) self.__delayTimer.setInterval(delay) self.__delayTimer.timeout.connect(self.__delayedAction) self.__minimized = False self.__minSize = 0 self.__maxSize = 0 self.__bigSize = QSize() self.splitter = None self.splitterSizes = [] self.__hasFocus = False # flag storing if this widget or any child has the focus self.__autoHide = False self.__tabBar.installEventFilter(self) self.__orientation = E5SideBar.North if orientation is None: orientation = E5SideBar.North self.setOrientation(orientation) self.__tabBar.currentChanged[int].connect( self.__stackedWidget.setCurrentIndex) e5App().focusChanged.connect(self.__appFocusChanged) self.__autoHideButton.toggled[bool].connect(self.__autoHideToggled) def setSplitter(self, splitter): """ Public method to set the splitter managing the sidebar. @param splitter reference to the splitter (QSplitter) """ self.splitter = splitter self.splitter.splitterMoved.connect(self.__splitterMoved) self.splitter.setChildrenCollapsible(False) index = self.splitter.indexOf(self) self.splitter.setCollapsible(index, False) def __splitterMoved(self, pos, index): """ Private slot to react on splitter moves. @param pos new position of the splitter handle (integer) @param index index of the splitter handle (integer) """ if self.splitter: self.splitterSizes = self.splitter.sizes() def __delayedAction(self): """ Private slot to handle the firing of the delay timer. """ if self.__actionMethod is not None: self.__actionMethod() def setDelay(self, delay): """ Public method to set the delay value for the expand/shrink delay in milliseconds. @param delay value for the expand/shrink delay in milliseconds (integer) """ self.__delayTimer.setInterval(delay) def delay(self): """ Public method to get the delay value for the expand/shrink delay in milliseconds. @return value for the expand/shrink delay in milliseconds (integer) """ return self.__delayTimer.interval() def __cancelDelayTimer(self): """ Private method to cancel the current delay timer. """ self.__delayTimer.stop() self.__actionMethod = None def shrink(self): """ Public method to record a shrink request. """ self.__delayTimer.stop() self.__actionMethod = self.__shrinkIt self.__delayTimer.start() def __shrinkIt(self): """ Private method to shrink the sidebar. """ self.__minimized = True self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() if self.splitter: self.splitterSizes = self.splitter.sizes() self.__stackedWidget.hide() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.setFixedHeight(self.__tabBar.minimumSizeHint().height()) else: self.setFixedWidth(self.__tabBar.minimumSizeHint().width()) self.__actionMethod = None def expand(self): """ Public method to record a expand request. """ self.__delayTimer.stop() self.__actionMethod = self.__expandIt self.__delayTimer.start() def __expandIt(self): """ Private method to expand the sidebar. """ self.__minimized = False self.__stackedWidget.show() self.resize(self.__bigSize) if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = max(self.__minSize, self.minimumSizeHint().height()) self.setMinimumHeight(minSize) self.setMaximumHeight(self.__maxSize) else: minSize = max(self.__minSize, self.minimumSizeHint().width()) self.setMinimumWidth(minSize) self.setMaximumWidth(self.__maxSize) if self.splitter: self.splitter.setSizes(self.splitterSizes) self.__actionMethod = None def isMinimized(self): """ Public method to check the minimized state. @return flag indicating the minimized state (boolean) """ return self.__minimized def isAutoHiding(self): """ Public method to check, if the auto hide function is active. @return flag indicating the state of auto hiding (boolean) """ return self.__autoHide def eventFilter(self, obj, evt): """ Public method to handle some events for the tabbar. @param obj reference to the object (QObject) @param evt reference to the event object (QEvent) @return flag indicating, if the event was handled (boolean) """ if obj == self.__tabBar: if evt.type() == QEvent.MouseButtonPress: pos = evt.pos() for i in range(self.__tabBar.count()): if self.__tabBar.tabRect(i).contains(pos): break if i == self.__tabBar.currentIndex(): if self.isMinimized(): self.expand() else: self.shrink() return True elif self.isMinimized(): self.expand() elif evt.type() == QEvent.Wheel: if qVersion() >= "5.0.0": delta = evt.angleDelta().y() else: delta = evt.delta() if delta > 0: self.prevTab() else: self.nextTab() return True return QWidget.eventFilter(self, obj, evt) def addTab(self, widget, iconOrLabel, label=None): """ Public method to add a tab to the sidebar. @param widget reference to the widget to add (QWidget) @param iconOrLabel reference to the icon or the label text of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.addTab(iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.addTab(iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.addWidget(widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def insertTab(self, index, widget, iconOrLabel, label=None): """ Public method to insert a tab into the sidebar. @param index the index to insert the tab at (integer) @param widget reference to the widget to insert (QWidget) @param iconOrLabel reference to the icon or the labeltext of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.insertTab(index, iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.insertTab(index, iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.insertWidget(index, widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def removeTab(self, index): """ Public method to remove a tab. @param index the index of the tab to remove (integer) """ self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index)) self.__tabBar.removeTab(index) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def clear(self): """ Public method to remove all tabs. """ while self.count() > 0: self.removeTab(0) def prevTab(self): """ Public slot used to show the previous tab. """ ind = self.currentIndex() - 1 if ind == -1: ind = self.count() - 1 self.setCurrentIndex(ind) self.currentWidget().setFocus() def nextTab(self): """ Public slot used to show the next tab. """ ind = self.currentIndex() + 1 if ind == self.count(): ind = 0 self.setCurrentIndex(ind) self.currentWidget().setFocus() def count(self): """ Public method to get the number of tabs. @return number of tabs in the sidebar (integer) """ return self.__tabBar.count() def currentIndex(self): """ Public method to get the index of the current tab. @return index of the current tab (integer) """ return self.__stackedWidget.currentIndex() def setCurrentIndex(self, index): """ Public slot to set the current index. @param index the index to set as the current index (integer) """ self.__tabBar.setCurrentIndex(index) self.__stackedWidget.setCurrentIndex(index) if self.isMinimized(): self.expand() def currentWidget(self): """ Public method to get a reference to the current widget. @return reference to the current widget (QWidget) """ return self.__stackedWidget.currentWidget() def setCurrentWidget(self, widget): """ Public slot to set the current widget. @param widget reference to the widget to become the current widget (QWidget) """ self.__stackedWidget.setCurrentWidget(widget) self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex()) if self.isMinimized(): self.expand() def indexOf(self, widget): """ Public method to get the index of the given widget. @param widget reference to the widget to get the index of (QWidget) @return index of the given widget (integer) """ return self.__stackedWidget.indexOf(widget) def isTabEnabled(self, index): """ Public method to check, if a tab is enabled. @param index index of the tab to check (integer) @return flag indicating the enabled state (boolean) """ return self.__tabBar.isTabEnabled(index) def setTabEnabled(self, index, enabled): """ Public method to set the enabled state of a tab. @param index index of the tab to set (integer) @param enabled enabled state to set (boolean) """ self.__tabBar.setTabEnabled(index, enabled) def orientation(self): """ Public method to get the orientation of the sidebar. @return orientation of the sidebar (North, East, South, West) """ return self.__orientation def setOrientation(self, orient): """ Public method to set the orientation of the sidebar. @param orient orientation of the sidebar (North, East, South, West) """ if orient == E5SideBar.North: self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.TopToBottom) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.East: self.__tabBar.setShape(QTabBar.RoundedEast) self.__tabBar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.RightToLeft) self.layout.setAlignment(self.barLayout, Qt.AlignTop) elif orient == E5SideBar.South: self.__tabBar.setShape(QTabBar.RoundedSouth) self.__tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.BottomToTop) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.West: self.__tabBar.setShape(QTabBar.RoundedWest) self.__tabBar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.LeftToRight) self.layout.setAlignment(self.barLayout, Qt.AlignTop) self.__orientation = orient def tabIcon(self, index): """ Public method to get the icon of a tab. @param index index of the tab (integer) @return icon of the tab (QIcon) """ return self.__tabBar.tabIcon(index) def setTabIcon(self, index, icon): """ Public method to set the icon of a tab. @param index index of the tab (integer) @param icon icon to be set (QIcon) """ self.__tabBar.setTabIcon(index, icon) def tabText(self, index): """ Public method to get the text of a tab. @param index index of the tab (integer) @return text of the tab (string) """ return self.__tabBar.tabText(index) def setTabText(self, index, text): """ Public method to set the text of a tab. @param index index of the tab (integer) @param text text to set (string) """ self.__tabBar.setTabText(index, text) def tabToolTip(self, index): """ Public method to get the tooltip text of a tab. @param index index of the tab (integer) @return tooltip text of the tab (string) """ return self.__tabBar.tabToolTip(index) def setTabToolTip(self, index, tip): """ Public method to set the tooltip text of a tab. @param index index of the tab (integer) @param tip tooltip text to set (string) """ self.__tabBar.setTabToolTip(index, tip) def tabWhatsThis(self, index): """ Public method to get the WhatsThis text of a tab. @param index index of the tab (integer) @return WhatsThis text of the tab (string) """ return self.__tabBar.tabWhatsThis(index) def setTabWhatsThis(self, index, text): """ Public method to set the WhatsThis text of a tab. @param index index of the tab (integer) @param text WhatsThis text to set (string) """ self.__tabBar.setTabWhatsThis(index, text) def widget(self, index): """ Public method to get a reference to the widget associated with a tab. @param index index of the tab (integer) @return reference to the widget (QWidget) """ return self.__stackedWidget.widget(index) def saveState(self): """ Public method to save the state of the sidebar. @return saved state as a byte array (QByteArray) """ if len(self.splitterSizes) == 0: if self.splitter: self.splitterSizes = self.splitter.sizes() self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() data = QByteArray() stream = QDataStream(data, QIODevice.WriteOnly) stream.setVersion(QDataStream.Qt_4_6) stream.writeUInt16(self.Version) stream.writeBool(self.__minimized) stream << self.__bigSize stream.writeUInt16(self.__minSize) stream.writeUInt16(self.__maxSize) stream.writeUInt16(len(self.splitterSizes)) for size in self.splitterSizes: stream.writeUInt16(size) stream.writeBool(self.__autoHide) return data def restoreState(self, state): """ Public method to restore the state of the sidebar. @param state byte array containing the saved state (QByteArray) @return flag indicating success (boolean) """ if state.isEmpty(): return False if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = self.layout.minimumSize().height() maxSize = self.maximumHeight() else: minSize = self.layout.minimumSize().width() maxSize = self.maximumWidth() data = QByteArray(state) stream = QDataStream(data, QIODevice.ReadOnly) stream.setVersion(QDataStream.Qt_4_6) stream.readUInt16() # version minimized = stream.readBool() if minimized and not self.__minimized: self.shrink() stream >> self.__bigSize self.__minSize = max(stream.readUInt16(), minSize) self.__maxSize = max(stream.readUInt16(), maxSize) count = stream.readUInt16() self.splitterSizes = [] for i in range(count): self.splitterSizes.append(stream.readUInt16()) self.__autoHide = stream.readBool() self.__autoHideButton.setChecked(not self.__autoHide) if not minimized: self.expand() return True ####################################################################### ## methods below implement the autohide functionality ####################################################################### def __autoHideToggled(self, checked): """ Private slot to handle the toggling of the autohide button. @param checked flag indicating the checked state of the button (boolean) """ self.__autoHide = not checked if self.__autoHide: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOn.png")) else: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) def __appFocusChanged(self, old, now): """ Private slot to handle a change of the focus. @param old reference to the widget, that lost focus (QWidget or None) @param now reference to the widget having the focus (QWidget or None) """ self.__hasFocus = self.isAncestorOf(now) if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() elif self.__autoHide and self.__hasFocus and self.isMinimized(): self.expand() def enterEvent(self, event): """ Protected method to handle the mouse entering this widget. @param event reference to the event (QEvent) """ if self.__autoHide and self.isMinimized(): self.expand() else: self.__cancelDelayTimer() def leaveEvent(self, event): """ Protected method to handle the mouse leaving this widget. @param event reference to the event (QEvent) """ if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() else: self.__cancelDelayTimer() def shutdown(self): """ Public method to shut down the object. This method does some preparations so the object can be deleted properly. It disconnects from the focusChanged signal in order to avoid trouble later on. """ e5App().focusChanged.disconnect(self.__appFocusChanged)
class DataSelectionGui(QWidget): """ Manages all GUI elements in the data selection applet. This class itself is the central widget and also owns/manages the applet drawer widgets. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget( self ): return self def appletDrawer( self ): return self._drawer def menus( self ): return [] def viewerControlWidget(self): return self._viewerControlWidgetStack def setImageIndex(self, imageIndex): if imageIndex is not None: self.laneSummaryTableView.selectRow(imageIndex) for detailWidget in self._detailViewerWidgets: detailWidget.selectRow(imageIndex) def stopAndCleanUp(self): self._cleaning_up = True for editor in list(self.volumeEditors.values()): self.viewerStack.removeWidget( editor ) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() ) editor.stopAndCleanUp() self.volumeEditors.clear() def imageLaneAdded(self, laneIndex): if len(self.laneSummaryTableView.selectedIndexes()) == 0: self.laneSummaryTableView.selectRow(laneIndex) # We don't have any real work to do because this gui initiated the lane addition in the first place if self.guiMode != GuiMode.Batch: if(len(self.topLevelOperator.DatasetGroup) != laneIndex+1): import warnings warnings.warn("DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.DatasetGroup), laneIndex)) def imageLaneRemoved(self, laneIndex, finalLength): # There's nothing to do here because the GUI already # handles operator resizes via slot callbacks. pass def allowLaneSelectionChange(self): return False ########################################### ########################################### class UserCancelledError(Exception): # This exception type is raised when the user cancels the # addition of dataset files in the middle of the process somewhere. # It isn't an error -- it's used for control flow. pass def __init__(self, parentApplet, dataSelectionOperator, serializer, instructionText, guiMode=GuiMode.Normal, max_lanes=None, show_axis_details=False): """ Constructor. :param dataSelectionOperator: The top-level operator. Must be of type :py:class:`OpMultiLaneDataSelectionGroup`. :param serializer: The applet's serializer. Must be of type :py:class:`DataSelectionSerializer` :param instructionText: A string to display in the applet drawer. :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``. Currently, there is no difference between normal and batch mode. :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow. If ``None``, there is no maximum. """ super(DataSelectionGui, self).__init__() self._cleaning_up = False self.parentApplet = parentApplet self._max_lanes = max_lanes self._default_h5n5_volumes = {} self.show_axis_details = show_axis_details self._viewerControls = QWidget() self.topLevelOperator = dataSelectionOperator self.guiMode = guiMode self.serializer = serializer self.threadRouter = ThreadRouter(self) self._initCentralUic() self._initAppletDrawerUic(instructionText) self._viewerControlWidgetStack = QStackedWidget(self) def handleImageRemove(multislot, index, finalLength): # Remove the viewer for this dataset datasetSlot = self.topLevelOperator.DatasetGroup[index] if datasetSlot in list(self.volumeEditors.keys()): editor = self.volumeEditors[datasetSlot] self.viewerStack.removeWidget( editor ) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() ) editor.stopAndCleanUp() self.topLevelOperator.DatasetGroup.notifyRemove( bind( handleImageRemove ) ) opWorkflow = self.topLevelOperator.parent assert hasattr(opWorkflow.shell, 'onSaveProjectActionTriggered'), \ "This class uses the IlastikShell.onSaveProjectActionTriggered function. Did you rename it?" def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0]+'/' uic.loadUi(localDir+"/dataSelection.ui", self) self._initTableViews() self._initViewerStack() self.splitter.setSizes( [150, 850] ) def _initAppletDrawerUic(self, instructionText): """ Load the ui file for the applet drawer, which we own. """ localDir = os.path.split(__file__)[0]+'/' self._drawer = uic.loadUi(localDir+"/dataSelectionDrawer.ui") self._drawer.instructionLabel.setText( instructionText ) def _initTableViews(self): self.fileInfoTabWidget.setTabText( 0, "Summary" ) self.laneSummaryTableView.setModel( DataLaneSummaryTableModel(self, self.topLevelOperator) ) self.laneSummaryTableView.dataLaneSelected.connect( self.showDataset ) self.laneSummaryTableView.addFilesRequested.connect( self.addFiles ) self.laneSummaryTableView.addStackRequested.connect( self.addStack ) self.laneSummaryTableView.removeLanesRequested.connect( self.handleRemoveLaneButtonClicked ) # These two helper functions enable/disable an 'add files' button for a given role # based on the the max lane index for that role and the overall permitted max_lanes def _update_button_status(viewer, role_index): if self._max_lanes: viewer.setEnabled( self._findFirstEmptyLane(role_index) < self._max_lanes ) def _handle_lane_added( button, role_index, lane_slot, lane_index ): def _handle_role_slot_added( role_slot, added_slot_index, *args ): if added_slot_index == role_index: role_slot.notifyReady( bind(_update_button_status, button, role_index) ) role_slot.notifyUnready( bind(_update_button_status, button, role_index) ) lane_slot[lane_index].notifyInserted( _handle_role_slot_added ) self._retained = [] # Retain menus so they don't get deleted self._detailViewerWidgets = [] for roleIndex, role in enumerate(self.topLevelOperator.DatasetRoles.value): detailViewer = DatasetDetailedInfoTableView(self) detailViewer.setModel(DatasetDetailedInfoTableModel(self, self.topLevelOperator, roleIndex)) self._detailViewerWidgets.append( detailViewer ) # Button detailViewer.addFilesRequested.connect( partial(self.addFiles, roleIndex)) detailViewer.addStackRequested.connect( partial(self.addStack, roleIndex)) detailViewer.addPrecomputedVolumeRequested.connect( partial(self.addPrecomputedVolume, roleIndex)) detailViewer.addRemoteVolumeRequested.connect( partial(self.addDvidVolume, roleIndex)) # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, detailViewer, roleIndex ) ) self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, detailViewer, roleIndex ) ) # While we're at it, do the same for the buttons in the summary table, too self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) ) self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) ) # Context menu detailViewer.replaceWithFileRequested.connect( partial(self.handleReplaceFile, roleIndex) ) detailViewer.replaceWithStackRequested.connect( partial(self.addStack, roleIndex) ) detailViewer.editRequested.connect( partial(self.editDatasetInfo, roleIndex) ) detailViewer.resetRequested.connect( partial(self.handleClearDatasets, roleIndex) ) # Drag-and-drop detailViewer.addFilesRequestedDrop.connect( partial( self.addFileNames, roleIndex=roleIndex ) ) # Selection handling def showFirstSelectedDataset( _roleIndex, lanes ): if lanes: self.showDataset( lanes[0], _roleIndex ) detailViewer.dataLaneSelected.connect( partial(showFirstSelectedDataset, roleIndex) ) self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role) self.fileInfoTabWidget.currentChanged.connect( self.handleSwitchTabs ) self.fileInfoTabWidget.setCurrentIndex(0) def handleSwitchTabs(self, tabIndex ): if tabIndex < len(self._detailViewerWidgets): roleIndex = tabIndex # If summary tab is moved to the front, change this line. detailViewer = self._detailViewerWidgets[roleIndex] selectedLanes = detailViewer.selectedLanes if selectedLanes: self.showDataset( selectedLanes[0], roleIndex ) def _initViewerStack(self): self.volumeEditors = {} self.viewerStack.addWidget( QWidget() ) def handleRemoveLaneButtonClicked(self): """ The user clicked the "Remove" button. Remove the currently selected row(s) from both the GUI and the top-level operator. """ # Figure out which lanes to remove selectedIndexes = self.laneSummaryTableView.selectedIndexes() rows = set() for modelIndex in selectedIndexes: rows.add( modelIndex.row() ) # Don't remove the last row, which is just buttons. rows.discard( self.laneSummaryTableView.model().rowCount()-1 ) # Remove in reverse order so row numbers remain consistent for row in reversed(sorted(rows)): # Remove lanes from the operator. # The table model will notice the changes and update the rows accordingly. finalSize = len(self.topLevelOperator.DatasetGroup) - 1 self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize) @threadRouted def showDataset(self, laneIndex, roleIndex=None): if self._cleaning_up: return if laneIndex == -1: self.viewerStack.setCurrentIndex(0) return assert threading.current_thread().name == "MainThread" if laneIndex >= len(self.topLevelOperator.DatasetGroup): return datasetSlot = self.topLevelOperator.DatasetGroup[laneIndex] # Create if necessary if datasetSlot not in list(self.volumeEditors.keys()): class DatasetViewer(LayerViewerGui): def moveToTop(self, roleIndex): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return datasetRoles = opLaneView.DatasetRoles.value if roleIndex >= len(datasetRoles): return roleName = datasetRoles[roleIndex] try: layerIndex = [l.name for l in self.layerstack].index(roleName) except ValueError: return else: self.layerstack.selectRow(layerIndex) self.layerstack.moveSelectedToTop() def setupLayers(self): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return [] layers = [] datasetRoles = opLaneView.DatasetRoles.value for roleIndex, slot in enumerate(opLaneView.ImageGroup): if slot.ready(): roleName = datasetRoles[roleIndex] layer = self.createStandardLayerFromSlot(slot) layer.name = roleName layers.append(layer) return layers opLaneView = self.topLevelOperator.getLane(laneIndex) layerViewer = DatasetViewer(self.parentApplet, opLaneView, crosshair=False) # Maximize the x-y view by default. layerViewer.volumeEditorWidget.quadview.ensureMaximized(2) self.volumeEditors[datasetSlot] = layerViewer self.viewerStack.addWidget( layerViewer ) self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget() ) # Show the right one viewer = self.volumeEditors[datasetSlot] displayedRole = self.fileInfoTabWidget.currentIndex() viewer.moveToTop(displayedRole) self.viewerStack.setCurrentWidget( viewer ) self._viewerControlWidgetStack.setCurrentWidget( viewer.viewerControlWidget() ) def handleReplaceFile(self, roleIndex, startingLane): self.addFiles(roleIndex, startingLane) def addFiles(self, roleIndex, startingLane=None): """ The user clicked the "Add File" button. Ask him to choose a file (or several) and add them to both the GUI table and the top-level operator inputs. """ # Find the directory of the most recently opened image file mostRecentImageFile = PreferencesManager().get( 'DataSelection', 'recent image' ) mostRecentImageFile = str(mostRecentImageFile) if mostRecentImageFile is not None: defaultDirectory = os.path.split(mostRecentImageFile)[0] else: defaultDirectory = os.path.expanduser('~') # Launch the "Open File" dialog fileNames = self.getImageFileNamesToOpen(self, defaultDirectory) # If the user didn't cancel if len(fileNames) > 0: PreferencesManager().set('DataSelection', 'recent image', fileNames[0]) try: self.addFileNames(fileNames, roleIndex, startingLane) except Exception as ex: log_exception( logger ) QMessageBox.critical(self, "Error loading file", str(ex)) @classmethod def getImageFileNamesToOpen(cls, parent_window, defaultDirectory): """ Launch an "Open File" dialog to ask the user for one or more image files. """ extensions = OpDataSelection.SupportedExtensions filter_strs = ["*." + x for x in extensions] filters = ["{filt} ({filt})".format(filt=x) for x in filter_strs] filt_all_str = "Image files (" + ' '.join(filter_strs) + ')' fileNames = [] file_dialog = QFileDialog( parent_window, caption="Select Images", directory=defaultDirectory, filter=filt_all_str) if ilastik_config.getboolean("ilastik", "debug"): # use Qt dialog in debug mode (more portable?) file_dialog.setOption(QFileDialog.DontUseNativeDialog, True) #file_dialog.setNameFilterDetailsVisible(False) # select multiple files file_dialog.setFileMode(QFileDialog.ExistingFiles) if not file_dialog.exec_(): return [] return cls.cleanFileList(file_dialog.selectedFiles()) @staticmethod def cleanFileList(fileList: typing.List[str]) -> typing.List[str]: fileNames = [pathlib.Path(selected_file) for selected_file in fileList] # For the n5 extension the attributes.json file has to be selected in the file dialog. # However we need just the *.n5 directory-file. for i, fileName in enumerate(fileNames): # On some OS's the open file dialog allows to return file names that do not exist assert fileName.exists(), \ f"The file '{fileName}' does not exist." if fileName.name.lower() == 'attributes.json' and any(p.suffix == ".n5" for p in fileName.parents): fileNames[i] = fileName.parent fileNames = [fileName.as_posix() for fileName in fileNames] return fileNames def _findFirstEmptyLane(self, roleIndex): opTop = self.topLevelOperator # Determine the number of files this role already has # Search for the last valid value. firstNewLane = 0 for laneIndex, slot in reversed(list(zip(list(range(len(opTop.DatasetGroup))), opTop.DatasetGroup))): if slot[roleIndex].ready(): firstNewLane = laneIndex+1 break return firstNewLane def addFileNames(self, fileNames, roleIndex, startingLane=None, rois=None): """ Add the given filenames to both the GUI table and the top-level operator inputs. If startingLane is None, the filenames will be *appended* to the role's list of files. If rois is provided, it must be a list of (start,stop) tuples (one for each fileName) """ # What lanes will we touch? startingLane, endingLane = self._determineLaneRange(fileNames, roleIndex, startingLane) if startingLane is None: # Something went wrong. return # If we're only adding new lanes, NOT modifying existing lanes... adding_only = startingLane == len(self.topLevelOperator) # Create a list of DatasetInfos try: infos = self._createDatasetInfos(roleIndex, fileNames, rois) except DataSelectionGui.UserCancelledError: return # If no exception was thrown so far, set up the operator now loaded_all = self._configureOpWithInfos(roleIndex, startingLane, endingLane, infos) if loaded_all: # Now check the resulting slots. # If they should be copied to the project file, say so. self._reconfigureDatasetLocations(roleIndex, startingLane, endingLane) self._checkDataFormatWarnings(roleIndex, startingLane, endingLane) # If we succeeded in adding all images, show the first one. self.showDataset(startingLane, roleIndex) # Notify the workflow that we just added some new lanes. if adding_only: workflow = self.parentApplet.topLevelOperator.parent workflow.handleNewLanesAdded() # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() self.updateInternalPathVisiblity() def _determineLaneRange(self, fileNames, roleIndex, startingLane=None): """ Determine which lanes should be configured if the user wants to add the given fileNames to the specified role, starting at startingLane. If startingLane is None, assume the user wants to APPEND the files to the role's slots. """ if startingLane is None or startingLane == -1: startingLane = len(self.topLevelOperator.DatasetGroup) endingLane = startingLane+len(fileNames)-1 else: assert startingLane < len(self.topLevelOperator.DatasetGroup) max_files = len(self.topLevelOperator.DatasetGroup) - \ startingLane if len(fileNames) > max_files: msg = "You selected {num_selected} files for {num_slots} "\ "slots. To add new files use the 'Add new...' option "\ "in the context menu or the button in the last row."\ .format(num_selected=len(fileNames), num_slots=max_files) QMessageBox.critical( self, "Too many files", msg ) return (None, None) endingLane = min(startingLane+len(fileNames)-1, len(self.topLevelOperator.DatasetGroup)) if self._max_lanes and endingLane >= self._max_lanes: msg = "You may not add more than {} file(s) to this workflow. Please try again.".format( self._max_lanes ) QMessageBox.critical( self, "Too many files", msg ) return (None, None) return (startingLane, endingLane) def _createDatasetInfos(self, roleIndex, filePaths, rois): """ Create a list of DatasetInfos for the given filePaths and rois rois may be None, in which case it is ignored. """ if rois is None: rois = [None]*len(filePaths) assert len(rois) == len(filePaths) infos = [] for filePath, roi in zip(filePaths, rois): info = self._createDatasetInfo(roleIndex, filePath, roi) infos.append(info) return infos def _createDatasetInfo(self, roleIndex, filePath, roi): """ Create a DatasetInfo object for the given filePath and roi. roi may be None, in which case it is ignored. """ cwd = self.topLevelOperator.WorkingDirectory.value datasetInfo = DatasetInfo(filePath, cwd=cwd) datasetInfo.subvolume_roi = roi # (might be None) absPath, relPath = getPathVariants(filePath, cwd) # If the file is in a totally different tree from the cwd, # then leave the path as absolute. Otherwise, override with the relative path. if relPath is not None and len(os.path.commonprefix([cwd, absPath])) > 1: datasetInfo.filePath = relPath h5Exts = ['.ilp', '.h5', '.hdf5'] n5Exts = ['.n5'] if os.path.splitext(datasetInfo.filePath)[1] in h5Exts + n5Exts: if os.path.splitext(datasetInfo.filePath)[1] in n5Exts: datasetNames = self.getPossibleN5InternalPaths( absPath ) else: datasetNames = self.getPossibleH5InternalPaths( absPath ) if len(datasetNames) == 0: raise RuntimeError("HDF5 file %s has no image datasets" % datasetInfo.filePath) elif len(datasetNames) == 1: datasetInfo.filePath += str(datasetNames[0]) else: # If exactly one of the file's datasets matches a user's previous choice, use it. if roleIndex not in self._default_h5n5_volumes: self._default_h5n5_volumes[roleIndex] = set() previous_selections = self._default_h5n5_volumes[roleIndex] possible_auto_selections = previous_selections.intersection(datasetNames) if len(possible_auto_selections) == 1: datasetInfo.filePath += str(list(possible_auto_selections)[0]) else: # Ask the user which dataset to choose dlg = H5N5VolumeSelectionDlg(datasetNames, self) if dlg.exec_() == QDialog.Accepted: selected_index = dlg.combo.currentIndex() selected_dataset = str(datasetNames[selected_index]) datasetInfo.filePath += selected_dataset self._default_h5n5_volumes[roleIndex].add(selected_dataset) else: raise DataSelectionGui.UserCancelledError() # Allow labels by default if this gui isn't being used for batch data. datasetInfo.allowLabels = ( self.guiMode == GuiMode.Normal ) return datasetInfo def _configureOpWithInfos(self, roleIndex, startingLane, endingLane, infos): """ Attempt to configure the specified role and lanes of the top-level operator with the given DatasetInfos. Returns True if all lanes were configured successfully, or False if something went wrong. """ opTop = self.topLevelOperator originalSize = len(opTop.DatasetGroup) # Resize the slot if necessary if len( opTop.DatasetGroup ) < endingLane+1: opTop.DatasetGroup.resize( endingLane+1 ) # Configure each subslot for laneIndex, info in zip(list(range(startingLane, endingLane+1)), infos): try: self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue( info ) except DatasetConstraintError as ex: return_val = [False] # Give the user a chance to fix the problem self.handleDatasetConstraintError(info, info.filePath, ex, roleIndex, laneIndex, return_val) if return_val[0]: # Successfully repaired graph. continue else: # Not successfully repaired. Roll back the changes self._opTopRemoveDset(originalSize, laneIndex, roleIndex) return False except OpDataSelection.InvalidDimensionalityError as ex: self._opTopRemoveDset(originalSize, laneIndex, roleIndex) QMessageBox.critical( self, "Dataset has different dimensionality", ex.message ) return False except Exception as ex: self._opTopRemoveDset(originalSize, laneIndex, roleIndex) msg = "Wasn't able to load your dataset into the workflow. See error log for details." log_exception( logger, msg ) QMessageBox.critical( self, "Dataset Load Error", msg ) return False return True def _opTopRemoveDset(self, laneNum, laneIndex, roleIndex): """ Removes a dataset in topLevelOperator and sets the number of lanes to laneNum :param laneNum: total number of lanes after the cleanup :param laneIndex: the lane index of the dataset which is to be removed :param roleIndex: role index of the dataset """ self.topLevelOperator.DatasetGroup.resize(laneNum) self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue(None) def _reconfigureDatasetLocations(self, roleIndex, startingLane, endingLane): """ Check the metadata for the given slots. If the data is stored a format that is poorly optimized for 3D access, then configure it to be copied to the project file. Finally, save the project if we changed something. """ save_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane+1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.prefer_2d and 'z' in output_slot.meta.axistags: shape = numpy.array(output_slot.meta.shape) total_volume = numpy.prod(shape) # Only copy to the project file if the total volume is reasonably small if total_volume < 0.5e9: info_slot = opTop.DatasetGroup[lane_index][roleIndex] info = info_slot.value info.location = DatasetInfo.Location.ProjectInternal info_slot.setValue( info, check_changed=False ) save_needed = True if save_needed: logger.info("Some of your data cannot be accessed efficiently in 3D in its current format." " It will now be copied to the project file.") opWorkflow = self.topLevelOperator.parent opWorkflow.shell.onSaveProjectActionTriggered() def _checkDataFormatWarnings(self, roleIndex, startingLane, endingLane): warn_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane+1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.inefficient_format: warn_needed = True if warn_needed: QMessageBox.warning( self, "Inefficient Data Format", "Your data cannot be accessed efficiently in its current format. " "Check the console output for details.\n" "(For HDF5 files, be sure to enable chunking on your dataset.)" ) @threadRouted def handleDatasetConstraintError(self, info, filename, ex, roleIndex, laneIndex, return_val=[False]): if ex.unfixable: msg = ( "Can't use dataset:\n\n" + filename + "\n\n" + "because it violates a constraint of the {} component.\n\n".format( ex.appletName ) + ex.message + "\n\n" ) QMessageBox.warning( self, "Can't use dataset", msg ) return_val[0] = False else: assert isinstance(ex, DatasetConstraintError) accepted = True while isinstance(ex, DatasetConstraintError) and accepted: msg = ( f"Can't use given properties for dataset:\n\n{filename}\n\nbecause it violates a constraint of " f"the {ex.appletName} component.\n\n{ex.message}\n\nIf possible, fix this problem by adjusting " f"the applet settings and or the dataset properties in the next window(s).") QMessageBox.warning(self, "Dataset Needs Correction", msg) for dlg in ex.fixing_dialogs: dlg() accepted, ex = self.repairDatasetInfo(info, roleIndex, laneIndex) # The success of this is 'returned' via our special out-param # (We can't return a value from this method because it is @threadRouted. return_val[0] = accepted and ex is None # successfully repaired def repairDatasetInfo(self, info, roleIndex, laneIndex): """Open the dataset properties editor and return True if the new properties are acceptable.""" defaultInfos = {} defaultInfos[laneIndex] = info editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, [laneIndex], defaultInfos, show_axis_details=self.show_axis_details) dlg_state, ex = editorDlg.exec_() return (dlg_state == QDialog.Accepted), ex @classmethod def getPossibleH5InternalPaths(cls, absPath, min_ndim=2, max_ndim=5): datasetNames = [] # Open the file as a read-only so we can get a list of the internal paths with h5py.File(absPath, 'r') as f: # Define a closure to collect all of the dataset names in the file. def accumulateDatasetPaths(name, val): if type(val) == h5py._hl.dataset.Dataset and min_ndim <= len(val.shape) <= max_ndim: datasetNames.append( '/' + name ) # Visit every group/dataset in the file f.visititems(accumulateDatasetPaths) return datasetNames @classmethod def getPossibleN5InternalPaths(cls, absPath, min_ndim=2, max_ndim=5): """ Returns the name of all datasets in the file with at least 2 axes. """ datasetNames = [] # Open the file as a read-only so we can get a list of the internal paths with z5py.N5File(absPath, mode='r+') as f: def accumulate_names(path, val): if isinstance(val, z5py.dataset.Dataset) and min_ndim <= len(val.shape) <= max_ndim: name = path.replace(absPath, '') # Need only the internal path here datasetNames.append(name) f.visititems(accumulate_names) return datasetNames def addStack(self, roleIndex, laneIndex): """ The user clicked the "Import Stack Files" button. """ stackDlg = StackFileSelectionWidget(self) stackDlg.exec_() if stackDlg.result() != QDialog.Accepted : return files = stackDlg.selectedFiles sequence_axis = stackDlg.sequence_axis if len(files) == 0: return cwd = self.topLevelOperator.WorkingDirectory.value info = DatasetInfo(os.path.pathsep.join(files), cwd=cwd) originalNumLanes = len(self.topLevelOperator.DatasetGroup) if laneIndex is None or laneIndex == -1: laneIndex = len(self.topLevelOperator.DatasetGroup) if len(self.topLevelOperator.DatasetGroup) < laneIndex+1: self.topLevelOperator.DatasetGroup.resize(laneIndex+1) def importStack(): self.parentApplet.busy = True self.parentApplet.appletStateUpdateRequested() # Serializer will update the operator for us, which will propagate to the GUI. try: self.serializer.importStackAsLocalDataset( info, sequence_axis ) try: self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue(info) except DatasetConstraintError as ex: # Give the user a chance to repair the problem. filename = files[0] + "\n...\n" + files[-1] return_val = [False] self.parentApplet.busy = False # required for possible fixing dialogs from DatasetConstraintError self.parentApplet.appletStateUpdateRequested() self.handleDatasetConstraintError( info, filename, ex, roleIndex, laneIndex, return_val ) if not return_val[0]: # Not successfully repaired. Roll back the changes and give up. self.topLevelOperator.DatasetGroup.resize(originalNumLanes) finally: self.parentApplet.busy = False self.parentApplet.appletStateUpdateRequested() req = Request( importStack ) req.notify_finished( lambda result: self.showDataset(laneIndex, roleIndex) ) req.notify_failed( partial(self.handleFailedStackLoad, files, originalNumLanes ) ) req.submit() @threadRouted def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info): msg = "Failed to load stack due to the following error:\n{}".format( exc ) msg += "\nAttempted stack files were:\n" msg += "\n".join(files) log_exception( logger, msg, exc_info ) QMessageBox.critical(self, "Failed to load image stack", msg) self.topLevelOperator.DatasetGroup.resize(originalNumLanes) def handleClearDatasets(self, roleIndex, selectedRows): for row in selectedRows: self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect() # Remove all operators that no longer have any connected slots laneIndexes = list(range( len(self.topLevelOperator.DatasetGroup))) for laneIndex, multislot in reversed(list(zip(laneIndexes, self.topLevelOperator.DatasetGroup))): any_ready = False for slot in multislot: any_ready |= slot.ready() if not any_ready: self.topLevelOperator.DatasetGroup.removeSlot( laneIndex, len(self.topLevelOperator.DatasetGroup)-1 ) # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() def editDatasetInfo(self, roleIndex, laneIndexes): editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, laneIndexes, show_axis_details=self.show_axis_details) editorDlg.exec_() self.parentApplet.appletStateUpdateRequested() def updateInternalPathVisiblity(self): for view in self._detailViewerWidgets: model = view.model() view.setColumnHidden(DatasetDetailedInfoColumn.InternalID, not model.hasInternalPaths()) def addPrecomputedVolume(self, roleIndex, laneIndex): # add history... history = [] browser = PrecomputedVolumeBrowser(history=history, parent=self) if browser.exec_() == PrecomputedVolumeBrowser.Rejected: return precomputed_url = browser.selected_url self.addFileNames([precomputed_url], roleIndex, laneIndex) def addDvidVolume(self, roleIndex, laneIndex): recent_hosts_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Hosts") recent_hosts = recent_hosts_pref.get() if not recent_hosts: recent_hosts = ["localhost:8000"] recent_hosts = [h for h in recent_hosts if h] # There used to be a bug where empty strings could be saved. Filter those out. recent_nodes_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Nodes") recent_nodes = recent_nodes_pref.get() or {} from .dvidDataSelectionBrowser import DvidDataSelectionBrowser browser = DvidDataSelectionBrowser(recent_hosts, recent_nodes, parent=self) if browser.exec_() == DvidDataSelectionBrowser.Rejected: return if None in browser.get_selection(): QMessageBox.critical("Couldn't use your selection.") return rois = None hostname, repo_uuid, volume_name, node_uuid, typename = browser.get_selection() dvid_url = 'http://{hostname}/api/node/{node_uuid}/{volume_name}'.format( **locals() ) subvolume_roi = browser.get_subvolume_roi() # Relocate host to top of 'recent' list, and limit list to 10 items. try: i = recent_hosts.index(hostname) del recent_hosts[i] except ValueError: pass finally: recent_hosts.insert(0, hostname) recent_hosts = recent_hosts[:10] # Save pref recent_hosts_pref.set(recent_hosts) recent_nodes[hostname] = node_uuid recent_nodes_pref.set(recent_nodes) if subvolume_roi is None: self.addFileNames([dvid_url], roleIndex, laneIndex) else: start, stop = subvolume_roi self.addFileNames([dvid_url], roleIndex, laneIndex, [(start, stop)])
class FontWindow(BaseWindow): def __init__(self, font, parent=None): super().__init__(parent) self._font = None self._infoWindow = None self._featuresWindow = None self._groupsWindow = None self._kerningWindow = None self._metricsWindow = None self.toolBar = ToolBar(self) self.toolBar.setTools(t() for t in QApplication.instance().drawingTools()) self.glyphCellView = GlyphCellView(self) self.glyphCellView.glyphActivated.connect(self.openGlyphTab) self.glyphCellView.glyphsDropped.connect(self._orderChanged) self.glyphCellView.selectionChanged.connect(self._selectionChanged) self.glyphCellView.setAcceptDrops(True) self.glyphCellView.setCellRepresentationName("TruFont.GlyphCell") self.glyphCellView.setFrameShape(self.glyphCellView.NoFrame) self.glyphCellView.setFocus() self.tabWidget = TabWidget(self) self.tabWidget.setAutoHide(True) self.tabWidget.setHeroFirstTab(True) self.tabWidget.addTab(self.tr("Font")) self.stackWidget = QStackedWidget(self) self.stackWidget.addWidget(self.glyphCellView) self.tabWidget.currentTabChanged.connect(self._tabChanged) self.tabWidget.tabRemoved.connect( lambda index: self.stackWidget.removeWidget(self.stackWidget.widget(index)) ) self.stackWidget.currentChanged.connect(self._widgetChanged) self.propertiesView = PropertiesView(font, self) self.propertiesView.hide() self.statusBar = StatusBar(self) self.statusBar.setMinimumSize(32) self.statusBar.setMaximumSize(128) self.statusBar.sizeChanged.connect(self._sizeChanged) self.setFont_(font) app = QApplication.instance() app.dispatcher.addObserver( self, "_drawingToolRegistered", "drawingToolRegistered" ) app.dispatcher.addObserver( self, "_drawingToolUnregistered", "drawingToolUnregistered" ) app.dispatcher.addObserver( self, "_glyphViewGlyphsChanged", "glyphViewGlyphsChanged" ) layout = QHBoxLayout(self) layout.addWidget(self.toolBar) vLayout = QVBoxLayout() vLayout.addWidget(self.tabWidget) pageWidget = PageWidget() pageWidget.addWidget(self.stackWidget) pageWidget.addWidget(self.statusBar) vLayout.addWidget(pageWidget) layout.addLayout(vLayout) layout.addWidget(self.propertiesView) layout.setContentsMargins(0, 2, 0, 0) layout.setSpacing(2) elements = [ ("Ctrl+D", self.deselect), (platformSpecific.closeKeySequence(), self.closeGlyphTab), # XXX: does this really not warrant widget focus? (QKeySequence.Delete, self.delete), ("Shift+" + QKeySequence(QKeySequence.Delete).toString(), self.delete), ("Z", lambda: self.zoom(1)), ("X", lambda: self.zoom(-1)), ] e = platformSpecific.altDeleteSequence() if e is not None: elements.append((e, self.delete)) e = platformSpecific.altRedoSequence() if e is not None: elements.append((e, self.redo)) for keys, callback in elements: shortcut = QShortcut(QKeySequence(keys), self) shortcut.activated.connect(callback) self.installEventFilter(PreviewEventFilter(self)) self.readSettings() self.propertiesView.activeLayerModified.connect(self._activeLayerModified) self.statusBar.sizeChanged.connect(self.writeSettings) def readSettings(self): geometry = settings.fontWindowGeometry() if geometry: self.restoreGeometry(geometry) cellSize = settings.glyphCellSize() self.statusBar.setSize(cellSize) hidden = settings.propertiesHidden() if not hidden: self.properties() def writeSettings(self): settings.setFontWindowGeometry(self.saveGeometry()) settings.setGlyphCellSize(self.glyphCellView.cellSize()[0]) settings.setPropertiesHidden(self.propertiesView.isHidden()) def menuBar(self): return self.layout().menuBar() def setMenuBar(self, menuBar): self.layout().setMenuBar(menuBar) def setupMenu(self, menuBar): app = QApplication.instance() fileMenu = menuBar.fetchMenu(Entries.File) fileMenu.fetchAction(Entries.File_New) fileMenu.fetchAction(Entries.File_Open) fileMenu.fetchMenu(Entries.File_Open_Recent) if not platformSpecific.mergeOpenAndImport(): fileMenu.fetchAction(Entries.File_Import) fileMenu.addSeparator() fileMenu.fetchAction(Entries.File_Save, self.saveFile) fileMenu.fetchAction(Entries.File_Save_As, self.saveFileAs) fileMenu.fetchAction(Entries.File_Save_All) fileMenu.fetchAction(Entries.File_Reload, self.reloadFile) fileMenu.addSeparator() fileMenu.fetchAction(Entries.File_Export, self.exportFile) fileMenu.fetchAction(Entries.File_Exit) editMenu = menuBar.fetchMenu(Entries.Edit) self._undoAction = editMenu.fetchAction(Entries.Edit_Undo, self.undo) self._redoAction = editMenu.fetchAction(Entries.Edit_Redo, self.redo) editMenu.addSeparator() cut = editMenu.fetchAction(Entries.Edit_Cut, self.cut) copy = editMenu.fetchAction(Entries.Edit_Copy, self.copy) copyComponent = editMenu.fetchAction( Entries.Edit_Copy_As_Component, self.copyAsComponent ) paste = editMenu.fetchAction(Entries.Edit_Paste, self.paste) self._clipboardActions = (cut, copy, copyComponent, paste) editMenu.fetchAction(Entries.Edit_Select_All, self.selectAll) # editMenu.fetchAction(Entries.Edit_Deselect, self.deselect) editMenu.fetchAction(Entries.Edit_Find, self.findGlyph) editMenu.addSeparator() editMenu.fetchAction(Entries.Edit_Settings) viewMenu = menuBar.fetchMenu(Entries.View) viewMenu.fetchAction(Entries.View_Zoom_In, lambda: self.zoom(1)) viewMenu.fetchAction(Entries.View_Zoom_Out, lambda: self.zoom(-1)) viewMenu.fetchAction(Entries.View_Reset_Zoom, self.resetZoom) viewMenu.addSeparator() viewMenu.fetchAction(Entries.View_Next_Tab, lambda: self.tabOffset(1)) viewMenu.fetchAction(Entries.View_Previous_Tab, lambda: self.tabOffset(-1)) viewMenu.fetchAction(Entries.View_Next_Glyph, lambda: self.glyphOffset(1)) viewMenu.fetchAction(Entries.View_Previous_Glyph, lambda: self.glyphOffset(-1)) viewMenu.fetchAction(Entries.View_Layer_Up, lambda: self.layerOffset(-1)) viewMenu.fetchAction(Entries.View_Layer_Down, lambda: self.layerOffset(1)) viewMenu.addSeparator() viewMenu.fetchAction(Entries.View_Show_Points) viewMenu.fetchAction(Entries.View_Show_Metrics) viewMenu.fetchAction(Entries.View_Show_Images) viewMenu.fetchAction(Entries.View_Show_Guidelines) fontMenu = menuBar.fetchMenu(Entries.Font) fontMenu.fetchAction(Entries.Font_Font_Info, self.fontInfo) fontMenu.fetchAction(Entries.Font_Font_Features, self.fontFeatures) fontMenu.addSeparator() fontMenu.fetchAction(Entries.Font_Add_Glyphs, self.addGlyphs) fontMenu.fetchAction(Entries.Font_Sort, self.sortGlyphs) # glyphMenu = menuBar.fetchMenu(self.tr("&Glyph")) # self._layerAction = glyphMenu.fetchAction( # self.tr("&Layer Actions…"), self.layerActions, "L") menuBar.fetchMenu(Entries.Scripts) windowMenu = menuBar.fetchMenu(Entries.Window) windowMenu.fetchAction(Entries.Window_Groups, self.groups) windowMenu.fetchAction(Entries.Window_Kerning, self.kerning) windowMenu.fetchAction(Entries.Window_Metrics, self.metrics) windowMenu.fetchAction(Entries.Window_Scripting) windowMenu.fetchAction(Entries.Window_Properties, self.properties) windowMenu.addSeparator() action = windowMenu.fetchAction(Entries.Window_Output) action.setEnabled(app.outputWindow is not None) helpMenu = menuBar.fetchMenu(Entries.Help) helpMenu.fetchAction(Entries.Help_Documentation) helpMenu.fetchAction(Entries.Help_Report_An_Issue) helpMenu.addSeparator() helpMenu.fetchAction(Entries.Help_About) self._updateGlyphActions() # -------------- # Custom methods # -------------- def font_(self): return self._font def setFont_(self, font): if self._font is not None: self._font.removeObserver(self, "Font.Changed") self._font.removeObserver(self, "Font.GlyphOrderChanged") self._font.removeObserver(self, "Font.SortDescriptorChanged") self._font = font self.setWindowTitle(self.fontTitle()) if font is None: return self._updateGlyphsFromGlyphOrder() font.addObserver(self, "_fontChanged", "Font.Changed") font.addObserver(self, "_glyphOrderChanged", "Font.GlyphOrderChanged") font.addObserver(self, "_sortDescriptorChanged", "Font.SortDescriptorChanged") def fontTitle(self): if self._font is None: return None path = self._font.path or self._font.binaryPath if path is not None: return os.path.basename(path.rstrip(os.sep)) return self.tr("Untitled") def isGlyphTab(self): return bool(self.stackWidget.currentIndex()) def openGlyphTab(self, glyph): # if a tab with this glyph exists already, switch to it for index in range(self.stackWidget.count()): if not index: continue view = self.stackWidget.widget(index) if list(view.glyphs()) == [glyph]: self.tabWidget.setCurrentTab(index) return # spawn widget = GlyphCanvasView(self) widget.setInputNames([glyph.name]) widget.activeGlyphChanged.connect(self._selectionChanged) widget.glyphNamesChanged.connect(self._namesChanged) widget.pointSizeModified.connect(self.statusBar.setSize) widget.toolModified.connect(self.toolBar.setCurrentTool) # add self.tabWidget.addTab(_textForGlyphs([glyph])) self.stackWidget.addWidget(widget) # activate self.tabWidget.setCurrentTab(-1) def closeGlyphTab(self): index = self.stackWidget.currentIndex() if index: self.tabWidget.removeTab(index) def maybeSaveBeforeExit(self): if self._font.dirty: ret = CloseMessageBox.getCloseDocument(self, self.fontTitle()) if ret == QMessageBox.Save: self.saveFile() return True elif ret == QMessageBox.Discard: return True return False return True # ------------- # Notifications # ------------- # app def _drawingToolRegistered(self, notification): toolClass = notification.data["tool"] index = self.stackWidget.currentIndex() parent = self.stackWidget.currentWidget() if index else None self.toolBar.addTool(toolClass(parent=parent)) def _drawingToolUnregistered(self, notification): toolClass = notification.data["tool"] for tool in self.toolBar.tools(): if isinstance(tool, toolClass): self.toolBar.removeTool(tool) return raise ValueError(f"couldn't find tool to unregister: {toolClass}") def _glyphViewGlyphsChanged(self, notification): self._updateGlyphActions() # widgets def _activeLayerModified(self): if self.isGlyphTab(): widget = self.stackWidget.currentWidget() index = self.sender().currentIndex().row() layers = self._font.layers layer = layers[layers.layerOrder[index]] currentGlyph = widget.activeGlyph() # XXX: adjust TLayer.get and use it if currentGlyph.name in layer: glyph = layer[currentGlyph.name] else: glyph = layer.newGlyph(currentGlyph.name) widget.setActiveGlyph(glyph) def _namesChanged(self): sender = self.sender() index = self.stackWidget.indexOf(sender) self.tabWidget.setTabName(index, _textForGlyphs(sender.glyphs())) def _sizeChanged(self): size = self.statusBar.size() if self.isGlyphTab(): widget = self.stackWidget.currentWidget() widget.setPointSize(size) else: self.glyphCellView.setCellSize(size) def _tabChanged(self, index): self.statusBar.setShouldPropagateSize(not index) # we need to hide, then setParent, then show self.stackWidget.currentWidget().hide() newWidget = self.stackWidget.widget(index) if index: for tool in self.toolBar.tools(): tool.setParent(newWidget) self.stackWidget.setCurrentIndex(index) newWidget.setFocus(Qt.OtherFocusReason) def _toolChanged(self, tool): widget = self.stackWidget.currentWidget() ok = widget.setCurrentTool(tool) # the glyph view NAKed the change (in mouseDown) # set back the current tool in the toolbar if not ok: self.toolBar.setCurrentTool(widget.currentTool()) def _widgetChanged(self, index): # update current glyph self._updateCurrentGlyph() # update undo/redo self._updateGlyphActions() # update slider if self.isGlyphTab(): lo, hi, unit = 0, 900000, " pt" widget = self.stackWidget.currentWidget() size = widget.pointSize() else: lo, hi, unit = 32, 128, None size = self.glyphCellView.cellSize()[0] self.statusBar.setMinimumSize(lo) self.statusBar.setMaximumSize(hi) self.statusBar.setSize(size) self.statusBar.setUnit(unit) self.statusBar.setTextVisible(not self.isGlyphTab()) # update and connect setCurrentTool try: self.toolBar.currentToolChanged.disconnect() except TypeError: pass if not index: return widget = self.stackWidget.currentWidget() widget.setCurrentTool(self.toolBar.currentTool()) self.toolBar.currentToolChanged.connect(self._toolChanged) def _orderChanged(self): # TODO: reimplement when we start showing glyph subsets glyphs = self.glyphCellView.glyphs() self._font.glyphOrder = [glyph.name for glyph in glyphs] def _selectionChanged(self): if self.isGlyphTab(): activeGlyph = self.stackWidget.currentWidget().activeGlyph() else: activeGlyph = self.glyphCellView.lastSelectedGlyph() # selection text # TODO: this should probably be internal to the label selection = self.glyphCellView.selection() if selection is not None: count = len(selection) if count == 1: glyph = self.glyphCellView.glyphsForIndexes(selection)[0] text = "%s " % glyph.name else: text = "" if count: text = self.tr(f"{text}(%n selected)", n=count) else: text = "" self.statusBar.setText(text) # currentGlyph app = QApplication.instance() app.setCurrentGlyph(activeGlyph) # actions self._updateGlyphActions() # defcon def _fontChanged(self, notification): font = notification.object self.setWindowModified(font.dirty) def _glyphOrderChanged(self, notification): self._updateGlyphsFromGlyphOrder() def _updateGlyphsFromGlyphOrder(self): font = self._font glyphOrder = font.glyphOrder if glyphOrder: glyphCount = 0 glyphs = [] for glyphName in glyphOrder: if glyphName in font: glyph = font[glyphName] glyphCount += 1 else: glyph = font.get(glyphName, asTemplate=True) glyphs.append(glyph) if glyphCount < len(font): # if some glyphs in the font are not present in the glyph # order, loop again to add them at the end for glyph in font: if glyph not in glyphs: glyphs.append(glyph) font.disableNotifications(observer=self) font.glyphOrder = [glyph.name for glyph in glyphs] font.enableNotifications(observer=self) else: glyphs = list(font) font.disableNotifications(observer=self) font.glyphOrder = [glyph.name for glyph in glyphs] font.enableNotifications(observer=self) self.glyphCellView.setGlyphs(glyphs) def _sortDescriptorChanged(self, notification): font = notification.object descriptors = notification.data["newValue"] if descriptors is None: return if descriptors[0]["type"] == "glyphSet": glyphNames = descriptors[0]["glyphs"] else: glyphNames = font.unicodeData.sortGlyphNames(font.keys(), descriptors) font.glyphOrder = glyphNames # ------------ # Menu methods # ------------ # File def saveFile(self, path=None, ufoFormatVersion=3): if path is None and self._font.path is None: self.saveFileAs() else: if path is None: path = self._font.path self._font.save(path, ufoFormatVersion) def saveFileAs(self): fileFormats = OrderedDict( [ (self.tr("UFO Font version 3 {}").format("(*.ufo)"), 3), (self.tr("UFO Font version 2 {}").format("(*.ufo)"), 2), ] ) state = settings.saveFileDialogState() path = self._font.path or self._font.binaryPath if path: directory = os.path.dirname(path) else: directory = ( None if state else QStandardPaths.standardLocations(QStandardPaths.DocumentsLocation)[ 0 ] ) # TODO: switch to directory dlg on platforms that need it dialog = QFileDialog( self, self.tr("Save File"), directory, ";;".join(fileFormats.keys()) ) if state: dialog.restoreState(state) dialog.setAcceptMode(QFileDialog.AcceptSave) if directory: dialog.setDirectory(directory) ok = dialog.exec_() settings.setSaveFileDialogState(dialog.saveState()) if ok: nameFilter = dialog.selectedNameFilter() path = dialog.selectedFiles()[0] if not os.path.basename(path).endswith(".ufo"): path += ".ufo" self.saveFile(path, fileFormats[nameFilter]) app = QApplication.instance() app.setCurrentFile(self._font.path) self.setWindowTitle(self.fontTitle()) # return ok def reloadFile(self): font = self._font path = font.path or font.binaryPath if not font.dirty or path is None: return if not ReloadMessageBox.getReloadDocument(self, self.fontTitle()): return if font.path is not None: font.reloadInfo() font.reloadKerning() font.reloadGroups() font.reloadFeatures() font.reloadLib() font.reloadGlyphs(font.keys()) font.dirty = False else: # TODO: we should do this in-place font_ = font.__class__().new() font_.extract(font.binaryPath) self.setFont_(font_) def exportFile(self): params, ok = ExportDialog.getExportParameters(self, self._font) if not ok: return baseName = params["baseName"] directory = params["exportDirectory"] compression = set(map(str.lower, params["compression"])) for format in map(str.lower, params["formats"]): fileName = f"{baseName}.{format}" path = os.path.join(directory, fileName) try: self._font.export(path, format, compression=compression) except Exception as e: msg = ( self.tr("This font’s feature file contains an error.") if isinstance(e, FeatureLibError) else None ) errorReports.showCriticalException(e, message=msg) # Edit def undo(self): widget = self.stackWidget.currentWidget() if self.isGlyphTab(): glyph = widget.activeGlyph() else: glyph = widget.lastSelectedGlyph() glyph.undo() def redo(self): widget = self.stackWidget.currentWidget() if self.isGlyphTab(): glyph = widget.activeGlyph() else: glyph = widget.lastSelectedGlyph() glyph.redo() def cut(self): self.copy() widget = self.stackWidget.currentWidget() if self.isGlyphTab(): glyph = widget.activeGlyph() deleteUISelection(glyph) else: glyphs = widget.glyphs() for index in widget.selection(): glyph = glyphs[index] glyph.clear() def copy(self): font = self._font widget = self.stackWidget.currentWidget() clipboard = QApplication.clipboard() mimeData = QMimeData() if self.isGlyphTab(): glyph = widget.activeGlyph() copyGlyph = glyph.getRepresentation("TruFont.FilterSelection") packGlyphs = (copyGlyph,) else: glyphs = self.glyphCellView.glyphs() packGlyphs = ( glyphs[index] for index in sorted(self.glyphCellView.selection()) ) svgGlyphs = [] pickled = [] for i, glyph in enumerate(packGlyphs): pickled.append(glyph.serialize(blacklist=("name", "unicodes"))) pen = SVGPathPen(font) glyph.draw(pen) col = i % 5 row = i // 5 g = '<g transform="matrix(1,0,0,-1,{:f},{:f})"><path d="{}"/></g>'.format( font.info.unitsPerEm * col, font.info.unitsPerEm * row, pen.getCommands(), ) svgGlyphs.append(g) mimeData.setData("application/x-trufont-glyph-data", pickle.dumps(pickled)) svg = """\ <?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> <svg version="1.0" xmlns="http://www.w3.org/2000/svg"> %s </svg> """ % "\n".join( svgGlyphs ) mimeData.setData("image/svg+xml", svg.encode("utf-8")) clipboard.setMimeData(mimeData) def copyAsComponent(self): if self.isGlyphTab(): pass else: glyphs = self.glyphCellView.glyphs() pickled = [] for index in self.glyphCellView.selection(): glyph = glyphs[index] componentGlyph = glyph.__class__() componentGlyph.width = glyph.width component = componentGlyph.instantiateComponent() component.baseGlyph = glyph.name pickled.append(componentGlyph.serialize()) clipboard = QApplication.clipboard() mimeData = QMimeData() mimeData.setData("application/x-trufont-glyph-data", pickle.dumps(pickled)) clipboard.setMimeData(mimeData) def paste(self): isGlyphTab = self.isGlyphTab() widget = self.stackWidget.currentWidget() if isGlyphTab: glyphs = (widget.activeGlyph(),) else: selection = self.glyphCellView.selection() glyphs = widget.glyphsForIndexes(selection) clipboard = QApplication.clipboard() mimeData = clipboard.mimeData() if mimeData.hasFormat("application/x-trufont-glyph-data"): data = pickle.loads(mimeData.data("application/x-trufont-glyph-data")) if len(data) == len(glyphs): for pickled, glyph in zip(data, glyphs): if isGlyphTab: pasteGlyph = glyph.__class__() pasteGlyph.deserialize(pickled) # TODO: if we serialize selected state, we don't need # to do this pasteGlyph.selected = True if ( len(pasteGlyph) or len(pasteGlyph.components) or len(pasteGlyph.anchors) ): glyph.beginUndoGroup() glyph.holdNotifications() count = len(glyph) pen = glyph.getPointPen() # contours, components pasteGlyph.drawPoints(pen) for contour in glyph[count:]: contour.selected = True # anchors for anchor in pasteGlyph.anchors: glyph.appendAnchor(dict(anchor)) # guidelines for guideline in pasteGlyph.guidelines: glyph.appendGuideline(dict(guideline)) glyph.releaseHeldNotifications() glyph.endUndoGroup() else: glyph.deserialize(pickled) return if mimeData.hasFormat("image/svg+xml"): if len(glyphs) == 1: glyph = glyphs[0] try: svgPath = SVGPath.fromstring(mimeData.data("image/svg+xml")) except Exception: pass else: glyph.beginUndoGroup() if not isGlyphTab: glyph.clear() svgPath.draw(glyph.getPen()) glyph.endUndoGroup() return if mimeData.hasText(): if len(glyphs) == 1: glyph = glyphs[0] otherGlyph = glyph.__class__() text = mimeData.text() try: readGlyphFromString(text, otherGlyph, otherGlyph.getPointPen()) except Exception: try: svgPath = SVGPath.fromstring(text) svgPath.draw(otherGlyph.getPen()) except Exception: return glyph.beginUndoGroup() if not isGlyphTab: glyph.clear() otherGlyph.drawPoints(glyph.getPointPen()) glyph.endUndoGroup() def selectAll(self): widget = self.stackWidget.currentWidget() if self.isGlyphTab(): glyph = widget.activeGlyph() if glyph.selected: for anchor in glyph.anchors: anchor.selected = True for component in glyph.components: component.selected = True else: glyph.selected = True else: widget.selectAll() def deselect(self): widget = self.stackWidget.currentWidget() if self.isGlyphTab(): glyph = widget.activeGlyph() for anchor in glyph.anchors: anchor.selected = False for component in glyph.components: component.selected = False glyph.selected = False else: widget.setSelection(set()) def delete(self): modifiers = QApplication.keyboardModifiers() widget = self.stackWidget.currentWidget() if self.isGlyphTab(): glyph = widget.activeGlyph() # TODO: fuse more the two methods, they're similar and delete is # Cut except not putting in the clipboard if modifiers & Qt.AltModifier: deleteUISelection(glyph) else: preserveShape = not modifiers & Qt.ShiftModifier removeUIGlyphElements(glyph, preserveShape) else: erase = modifiers & Qt.ShiftModifier if self._proceedWithDeletion(erase): glyphs = widget.glyphsForIndexes(widget.selection()) for glyph in glyphs: font = glyph.font for layer in font.layers: if glyph.name in layer: defaultLayer = layer[glyph.name] == glyph if defaultLayer and not erase: # TODO: clear in glyph.template setter? glyph.clear() glyph.template = True else: del layer[glyph.name] def findGlyph(self): widget = self.stackWidget.currentWidget() if self.isGlyphTab(): glyph = widget.activeGlyph() newGlyph, ok = FindDialog.getNewGlyph(self, glyph) if ok and newGlyph is not None: widget.setActiveGlyph(newGlyph) else: pass # XXX # View def zoom(self, step): if self.isGlyphTab(): widget = self.stackWidget.currentWidget() newScale = widget.scale() * pow(1.2, step) widget.zoom(newScale) self.statusBar.setSize(widget.pointSize()) else: value = self.statusBar.size() newValue = value + 10 * step self.statusBar.setSize(newValue) def resetZoom(self): widget = self.stackWidget.currentWidget() if self.isGlyphTab(): widget.fitScaleBBox() else: settings.removeGlyphCellSize() cellSize = settings.glyphCellSize() self.statusBar.setSize(cellSize) def tabOffset(self, value): tab = self.tabWidget.currentTab() newTab = (tab + value) % len(self.tabWidget.tabs()) self.tabWidget.setCurrentTab(newTab) def glyphOffset(self, value): widget = self.stackWidget.currentWidget() if self.isGlyphTab(): currentGlyph = widget.activeGlyph() font = currentGlyph.font glyphOrder = font.glyphOrder # should be enforced in fontView already if not (glyphOrder and len(glyphOrder)): return index = glyphOrder.index(currentGlyph.name) newIndex = (index + value) % len(glyphOrder) glyph = font[glyphOrder[newIndex]] widget.setActiveGlyph(glyph) else: lastSelectedCell = widget.lastSelectedCell() if lastSelectedCell is None: return newIndex = lastSelectedCell + value if newIndex < 0 or newIndex >= len(widget.glyphs()): return widget.setSelection({newIndex}) def layerOffset(self, value): widget = self.stackWidget.currentWidget() if self.isGlyphTab(): currentGlyph = widget.activeGlyph() layerSet, layer = currentGlyph.layerSet, currentGlyph.layer if None in (layerSet, layer): return index = layerSet.layerOrder.index(layer.name) newIndex = (index + value) % len(layerSet) layer_ = layerSet[layerSet.layerOrder[newIndex]] if layer_ == layer: return # XXX: fix get # glyph = layer_.get(currentGlyph.name) if currentGlyph.name in layer_: glyph = layer_[currentGlyph.name] else: glyph = layer_.newGlyph(currentGlyph.name) widget.setActiveGlyph(glyph) # Font def fontInfo(self): # If a window is already opened, bring it to the front, else spawn one. # TODO: see about using widget.setAttribute(Qt.WA_DeleteOnClose) # otherwise it seems we're just leaking memory after each close... # (both raise_ and show allocate memory instead of using the hidden # widget it seems) if self._infoWindow is not None and self._infoWindow.isVisible(): self._infoWindow.raise_() else: self._infoWindow = FontInfoWindow(self._font, self) self._infoWindow.show() def fontFeatures(self): # TODO: see up here if self._featuresWindow is not None and self._featuresWindow.isVisible(): self._featuresWindow.raise_() else: self._featuresWindow = FontFeaturesWindow(self._font, self) self._featuresWindow.show() def addGlyphs(self): glyphs = self.glyphCellView.glyphs() newGlyphNames, params, ok = AddGlyphsDialog.getNewGlyphNames(self, glyphs) if ok: sortFont = params.pop("sortFont") for name in newGlyphNames: glyph = self._font.get(name, **params) if glyph is not None: glyphs.append(glyph) self.glyphCellView.setGlyphs(glyphs) if sortFont: # TODO: when the user add chars from a glyphSet and no others, # should we try to sort according to that glyphSet? # The above would probably warrant some rearchitecturing. # kick-in the sort mechanism self._font.sortDescriptor = self._font.sortDescriptor def sortGlyphs(self): sortDescriptor, ok = SortDialog.getDescriptor(self, self._font.sortDescriptor) if ok: self._font.sortDescriptor = sortDescriptor # Window def groups(self): # TODO: see up here if self._groupsWindow is not None and self._groupsWindow.isVisible(): self._groupsWindow.raise_() else: self._groupsWindow = GroupsWindow(self._font, self) self._groupsWindow.show() def kerning(self): # TODO: see up here if self._kerningWindow is not None and self._kerningWindow.isVisible(): self._kerningWindow.raise_() else: self._kerningWindow = KerningWindow(self._font, self) self._kerningWindow.show() def metrics(self): # TODO: see up here if self._metricsWindow is not None and self._metricsWindow.isVisible(): self._metricsWindow.raise_() else: self._metricsWindow = MetricsWindow(self._font) # XXX: need proper, fast windowForFont API! self._metricsWindow._fontWindow = self self.destroyed.connect(self._metricsWindow.close) self._metricsWindow.show() # TODO: default string kicks-in on the window before this. Figure out # how to make a clean interface selection = self.glyphCellView.selection() if selection: glyphs = self.glyphCellView.glyphsForIndexes(selection) self._metricsWindow.setGlyphs(glyphs) def properties(self): shouldBeVisible = self.propertiesView.isHidden() self.propertiesView.setVisible(shouldBeVisible) self.writeSettings() # update methods def _setGlyphPreview(self, value): index = self.stackWidget.currentIndex() if index: widget = self.stackWidget.currentWidget() widget.setPreviewEnabled(value) def _updateCurrentGlyph(self): # TODO: refactor this pattern... widget = self.stackWidget.currentWidget() if self.isGlyphTab(): glyph = widget.activeGlyph() else: glyph = widget.lastSelectedGlyph() if glyph is not None: app = QApplication.instance() app.setCurrentGlyph(glyph) def _updateGlyphActions(self): if not hasattr(self, "_undoAction"): return widget = self.stackWidget.currentWidget() if self.isGlyphTab(): currentGlyph = widget.activeGlyph() else: currentGlyph = widget.lastSelectedGlyph() # disconnect eventual signal of previous glyph objects = ((self._undoAction, self.undo), (self._redoAction, self.redo)) for action, slot in objects: try: action.disconnect() except TypeError: pass action.triggered.connect(slot) # now update status if currentGlyph is None: self._undoAction.setEnabled(False) self._redoAction.setEnabled(False) else: undoManager = currentGlyph.undoManager self._undoAction.setEnabled(currentGlyph.canUndo()) undoManager.canUndoChanged.connect(self._undoAction.setEnabled) self._redoAction.setEnabled(currentGlyph.canRedo()) undoManager.canRedoChanged.connect(self._redoAction.setEnabled) # and other actions for action in self._clipboardActions: action.setEnabled(currentGlyph is not None) # helper def _proceedWithDeletion(self, erase=False): if not self.glyphCellView.selection(): return tr = self.tr("Delete") if erase else self.tr("Clear") text = self.tr("Do you want to %s selected glyphs?") % tr.lower() closeDialog = QMessageBox( QMessageBox.Question, "", self.tr("%s glyphs") % tr, QMessageBox.Yes | QMessageBox.No, self, ) closeDialog.setInformativeText(text) closeDialog.setModal(True) ret = closeDialog.exec_() if ret == QMessageBox.Yes: return True return False # ---------- # Qt methods # ---------- def setWindowTitle(self, title): if platformSpecific.appNameInTitle(): title += " – TruFont" super().setWindowTitle(f"[*]{title}") def sizeHint(self): return QSize(1270, 800) def moveEvent(self, event): self.writeSettings() resizeEvent = moveEvent def showEvent(self, event): app = QApplication.instance() data = dict(font=self._font, window=self) app.postNotification("fontWindowWillOpen", data) super().showEvent(event) app.postNotification("fontWindowOpened", data) def closeEvent(self, event): ok = self.maybeSaveBeforeExit() if ok: app = QApplication.instance() data = dict(font=self._font, window=self) app.postNotification("fontWindowWillClose", data) self._font.removeObserver(self, "Font.Changed") app = QApplication.instance() app.dispatcher.removeObserver(self, "drawingToolRegistered") app.dispatcher.removeObserver(self, "drawingToolUnregistered") app.dispatcher.removeObserver(self, "glyphViewGlyphsChanged") event.accept() else: event.ignore() def event(self, event): if event.type() == QEvent.WindowActivate: app = QApplication.instance() app.setCurrentFontWindow(self) self._updateCurrentGlyph() return super().event(event) def paintEvent(self, event): painter = QPainter(self) painter.fillRect(event.rect(), QColor(212, 212, 212))
class Toolbox(QWidget): """A side-oriented widget similar to a TabWidget that can be collapsed and expanded. A Toolbox is designed to be a container for sets of controls, grouped into 'pages' and accessible by a TabBar, in the same way as a TabWidget. A page is normally a QWidget with a layout that contains controls. A widget can be added as a new tab using :meth:`addTab`. The Toolbox has slots for triggering its collapse and expansion, both in an animated mode (soft slide) and a 'quick' mode which skips the animation. Commonly the collapse/expand slots are connected to the tabBar's :meth:`tabBarDoubleClicked` signal. Normally in the DataLogger a Toolbox is created and then added to a :class:`~cued_datalogger.api.toolbox.MasterToolbox`, which connects the relevant signals for collapsing and expanding the Toolbox. Attributes ---------- tabBar : QTabBar tabPages : QStackedWidget The stack of widgets that form the pages of the tabs. collapse_animation : QPropertyAnimation The animation that controls how the Toolbox collapses. """ sig_collapsed_changed = pyqtSignal() def __init__(self, widget_side='left', parent=None): self.parent = parent self.widget_side = widget_side super().__init__(parent) self.layout = QHBoxLayout() self.layout.setSpacing(0) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # # Create the tab bar self.tabBar = QTabBar(self) self.tabBar.setTabsClosable(False) self.tabBar.setMovable(False) self.tabBar.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) # # Create the Stacked widget for the pages self.tabPages = QStackedWidget(self) # # Link the signals so that changing tab leads to a change of page self.tabBar.currentChanged.connect(self.changePage) # # Add them to the splitter (self) # Right side orientation if self.widget_side == 'right': self.tabBar.setShape(QTabBar.RoundedWest) self.layout.addWidget(self.tabBar) self.layout.addWidget(self.tabPages) # Left side orientation else: self.tabBar.setShape(QTabBar.RoundedEast) self.layout.addWidget(self.tabPages) self.layout.addWidget(self.tabBar) self.setLayout(self.layout) self.collapsed = False self.expanded_width = self.sizeHint().width() def addTab(self, widget, title): """Add a new tab, with the page widget *widget* and tab title *title*.""" self.tabBar.addTab(title) self.tabPages.addWidget(widget) def removeTab(self, title): """Remove the tab with title *title*.""" for tab_num in range(self.tabBar.count()): if self.tabBar.tabText(tab_num) == title: self.tabBar.removeTab(tab_num) self.tabPages.removeWidget(self.tabPages.widget(tab_num)) def toggle_collapse(self): """If collapsed, expand the widget so the pages are visible. If not collapsed, collapse the widget so that only the tabBar is showing.""" # If collapsed, expand if self.collapsed: self.expand() # If expanded, collapse: else: self.collapse() def expand(self): """Expand the widget so that the pages are visible.""" self.tabPages.show() self.sig_collapsed_changed.emit() self.collapsed = False def collapse(self): """Collapse the widget so that only the tab bar is visible.""" self.tabPages.hide() self.sig_collapsed_changed.emit() self.collapsed = True def changePage(self, index): """Set the current page to *index*.""" self.tabBar.setCurrentIndex(index) self.tabPages.setCurrentIndex(index) if self.tabPages.currentWidget(): self.tabPages.currentWidget().resize(self.tabPages.size()) def clear(self): """Remove all tabs and pages.""" for i in range(self.tabBar.count()): # Remove the tab and page at position 0 self.tabBar.removeTab(0) self.tabPages.removeWidget(self.tabPages.currentWidget())
class TabBarWindow(TabWindow): """Implementation which uses a separate QTabBar and QStackedWidget. The Tab bar is placed next to the menu bar to save real estate.""" def __init__(self, app, **kwargs): super().__init__(app, **kwargs) def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(640, 480) self.tabBar = QTabBar() self.verticalLayout = QVBoxLayout() self.verticalLayout.setContentsMargins(0, 0, 0, 0) self._setupActions() self._setupMenu() self.centralWidget = QWidget(self) self.setCentralWidget(self.centralWidget) self.stackedWidget = QStackedWidget() self.centralWidget.setLayout(self.verticalLayout) self.horizontalLayout = QHBoxLayout() self.horizontalLayout.addWidget(self.menubar, 0, Qt.AlignTop) self.horizontalLayout.addWidget(self.tabBar, 0, Qt.AlignTop) self.verticalLayout.addLayout(self.horizontalLayout) self.verticalLayout.addWidget(self.stackedWidget) self.tabBar.currentChanged.connect(self.showWidget) self.tabBar.tabCloseRequested.connect(self.onTabCloseRequested) self.stackedWidget.currentChanged.connect(self.updateMenuBar) self.stackedWidget.widgetRemoved.connect(self.onRemovedWidget) self.tabBar.setTabsClosable(True) self.restoreGeometry() def addTab(self, page, title, switch=True): stack_index = self.stackedWidget.insertWidget(-1, page) tab_index = self.tabBar.addTab(title) if isinstance(page, DirectoriesDialog): self.tabBar.setTabButton(tab_index, QTabBar.RightSide, None) if switch: # switch to the added tab immediately upon creation self.setTabIndex(tab_index) self.stackedWidget.setCurrentWidget(page) return stack_index @pyqtSlot(int) def showWidget(self, index): if index >= 0 and index <= self.stackedWidget.count() - 1: self.stackedWidget.setCurrentIndex(index) # if not self.tabBar.isTabVisible(index): self.setTabVisible(index, True) def indexOfWidget(self, widget): # Warning: this may return -1 if widget is not a child of stackedwidget return self.stackedWidget.indexOf(widget) def setCurrentIndex(self, tab_index): # The signal will handle switching the stackwidget's widget self.setTabIndex(tab_index) # self.stackedWidget.setCurrentWidget(self.stackedWidget.widget(tab_index)) @pyqtSlot(int) def setTabIndex(self, index): if index is None: return self.tabBar.setCurrentIndex(index) def setTabVisible(self, index, value): return self.tabBar.setTabVisible(index, value) @pyqtSlot(int) def onRemovedWidget(self, index): self.removeTab(index) @pyqtSlot(int) def removeTab(self, index): # No need to remove the widget here: # self.stackedWidget.removeWidget(self.stackedWidget.widget(index)) return self.tabBar.removeTab(index) @pyqtSlot(int) def removeWidget(self, widget): return self.stackedWidget.removeWidget(widget) def isTabVisible(self, index): return self.tabBar.isTabVisible(index) def getCurrentIndex(self): return self.stackedWidget.currentIndex() def getWidgetAtIndex(self, index): return self.stackedWidget.widget(index) def getCount(self): return self.stackedWidget.count() @pyqtSlot() def toggleTabBar(self): value = self.sender().isChecked() self.actionToggleTabs.setChecked(value) self.tabBar.setVisible(value) @pyqtSlot(int) def onTabCloseRequested(self, index): current_widget = self.getWidgetAtIndex(index) if isinstance(current_widget, DirectoriesDialog): # On MacOS, the tab has a close button even though we explicitely # set it to None in order to hide it. This should prevent # the "Directories" tab from closing by mistake. return current_widget.close() self.stackedWidget.removeWidget(current_widget)
class XTabWidget(QFrame): addClicked = pyqtSignal() currentChanged = pyqtSignal(int) tabCloseRequested = pyqtSignal(int) def __init__(self, QWidget_parent=None): super(XTabWidget, self).__init__(QWidget_parent) # setup self frame self.setFrameShadow(QFrame.Raised) # self.setFrameShape(QFrame.StyledPanel) self.setFrameShape(QFrame.NoFrame) # layouts self._layout = QVBoxLayout() self._layout.setContentsMargins(0, 0, 0, 0) self._layout.setSpacing(2) self._layout_top = QHBoxLayout() self._layout_top.setContentsMargins(0, 0, 0, 0) # stacked widget self._stack = QStackedWidget(self) # tab bar self._tabbar = QTabBar(self) self._tabbar.setTabsClosable(True) self._tabbar.setMovable(False) self._tabbar.setExpanding(False) self._tabbar.setShape(QTabBar.RoundedNorth) self._tabbar.currentChanged.connect(self.on_tab_current_changed) self._tabbar.tabCloseRequested.connect(self.on_tab_close_requested) # button "add" self._btn_add = QPushButton('+', self) self._btn_add.setMaximumSize(QSize(22, 22)) self._btn_add.clicked.connect(self.on_btn_add_clicked) # complete layout self._layout_top.addWidget(self._btn_add, 0, Qt.AlignVCenter) self._layout_top.addWidget(self._tabbar, 1, Qt.AlignVCenter) self._layout.addLayout(self._layout_top) self._layout.addWidget(self._stack) self.setLayout(self._layout) def addTab(self, widget: QWidget, title: str, closeable: bool = True) -> int: # add tab to tabbar tab_index = self._tabbar.addTab(title) if not closeable: self._tabbar.setTabButton(tab_index, QTabBar.RightSide, None) self._tabbar.setTabButton(tab_index, QTabBar.LeftSide, None) # it MAY be on the left too!! # add widget into stackedwidget self._stack.addWidget(widget) return tab_index def removeTab(self, index: int): # remove from tab bar self._tabbar.removeTab(index) # remove from stacked widget widget = self._stack.widget(index) if widget is not None: # Removes widget from the QStackedWidget. i.e., widget # is not deleted but simply removed from the stacked layout, # causing it to be hidden. self._stack.removeWidget(widget) # and now we probably want to delete it to avoid memory leak widget.close() widget.deleteLater() def tabBar(self) -> QTabBar: return self._tabbar def enableButtonAdd(self, enableState: bool = True): self._btn_add.setEnabled(enableState) def setCurrentIndex(self, index: int): self._stack.setCurrentIndex(index) self._tabbar.setCurrentIndex(index) def count(self) -> int: return self._tabbar.count() def tabWidget(self, index: int): """ Return page widget, inserted at index index :param index: :return: QWidget inserted at specified index, or None """ widget = self._stack.widget(index) return widget @pyqtSlot() def on_btn_add_clicked(self): self.addClicked.emit() @pyqtSlot(int) def on_tab_current_changed(self, idx: int): self._stack.setCurrentIndex(idx) self.currentChanged.emit(idx) @pyqtSlot(int) def on_tab_close_requested(self, idx: int): self.tabCloseRequested.emit(idx)
class TilesetDock(QDockWidget): ## # Emitted when the current tile changed. ## currentTileChanged = pyqtSignal(list) ## # Emitted when the currently selected tiles changed. ## stampCaptured = pyqtSignal(TileStamp) ## # Emitted when files are dropped at the tileset dock. ## tilesetsDropped = pyqtSignal(QStringList) newTileset = pyqtSignal() ## # Constructor. ## def __init__(self, parent=None): super().__init__(parent) # Shared tileset references because the dock wants to add new tiles self.mTilesets = QVector() self.mCurrentTilesets = QMap() self.mMapDocument = None self.mTabBar = QTabBar() self.mViewStack = QStackedWidget() self.mToolBar = QToolBar() self.mCurrentTile = None self.mCurrentTiles = None self.mNewTileset = QAction(self) self.mImportTileset = QAction(self) self.mExportTileset = QAction(self) self.mPropertiesTileset = QAction(self) self.mDeleteTileset = QAction(self) self.mEditTerrain = QAction(self) self.mAddTiles = QAction(self) self.mRemoveTiles = QAction(self) self.mTilesetMenuButton = TilesetMenuButton(self) self.mTilesetMenu = QMenu(self) # opens on click of mTilesetMenu self.mTilesetActionGroup = QActionGroup(self) self.mTilesetMenuMapper = None # needed due to dynamic content self.mEmittingStampCaptured = False self.mSynchronizingSelection = False self.setObjectName("TilesetDock") self.mTabBar.setMovable(True) self.mTabBar.setUsesScrollButtons(True) self.mTabBar.currentChanged.connect(self.updateActions) self.mTabBar.tabMoved.connect(self.moveTileset) w = QWidget(self) horizontal = QHBoxLayout() horizontal.setSpacing(0) horizontal.addWidget(self.mTabBar) horizontal.addWidget(self.mTilesetMenuButton) vertical = QVBoxLayout(w) vertical.setSpacing(0) vertical.setContentsMargins(5, 5, 5, 5) vertical.addLayout(horizontal) vertical.addWidget(self.mViewStack) horizontal = QHBoxLayout() horizontal.setSpacing(0) horizontal.addWidget(self.mToolBar, 1) vertical.addLayout(horizontal) self.mNewTileset.setIcon(QIcon(":images/16x16/document-new.png")) self.mImportTileset.setIcon(QIcon(":images/16x16/document-import.png")) self.mExportTileset.setIcon(QIcon(":images/16x16/document-export.png")) self.mPropertiesTileset.setIcon( QIcon(":images/16x16/document-properties.png")) self.mDeleteTileset.setIcon(QIcon(":images/16x16/edit-delete.png")) self.mEditTerrain.setIcon(QIcon(":images/16x16/terrain.png")) self.mAddTiles.setIcon(QIcon(":images/16x16/add.png")) self.mRemoveTiles.setIcon(QIcon(":images/16x16/remove.png")) Utils.setThemeIcon(self.mNewTileset, "document-new") Utils.setThemeIcon(self.mImportTileset, "document-import") Utils.setThemeIcon(self.mExportTileset, "document-export") Utils.setThemeIcon(self.mPropertiesTileset, "document-properties") Utils.setThemeIcon(self.mDeleteTileset, "edit-delete") Utils.setThemeIcon(self.mAddTiles, "add") Utils.setThemeIcon(self.mRemoveTiles, "remove") self.mNewTileset.triggered.connect(self.newTileset) self.mImportTileset.triggered.connect(self.importTileset) self.mExportTileset.triggered.connect(self.exportTileset) self.mPropertiesTileset.triggered.connect(self.editTilesetProperties) self.mDeleteTileset.triggered.connect(self.removeTileset) self.mEditTerrain.triggered.connect(self.editTerrain) self.mAddTiles.triggered.connect(self.addTiles) self.mRemoveTiles.triggered.connect(self.removeTiles) self.mToolBar.addAction(self.mNewTileset) self.mToolBar.setIconSize(QSize(16, 16)) self.mToolBar.addAction(self.mImportTileset) self.mToolBar.addAction(self.mExportTileset) self.mToolBar.addAction(self.mPropertiesTileset) self.mToolBar.addAction(self.mDeleteTileset) self.mToolBar.addAction(self.mEditTerrain) self.mToolBar.addAction(self.mAddTiles) self.mToolBar.addAction(self.mRemoveTiles) self.mZoomable = Zoomable(self) self.mZoomComboBox = QComboBox() self.mZoomable.connectToComboBox(self.mZoomComboBox) horizontal.addWidget(self.mZoomComboBox) self.mViewStack.currentChanged.connect(self.updateCurrentTiles) TilesetManager.instance().tilesetChanged.connect(self.tilesetChanged) DocumentManager.instance().documentAboutToClose.connect( self.documentAboutToClose) self.mTilesetMenuButton.setMenu(self.mTilesetMenu) self.mTilesetMenu.aboutToShow.connect(self.refreshTilesetMenu) self.setWidget(w) self.retranslateUi() self.setAcceptDrops(True) self.updateActions() def __del__(self): del self.mCurrentTiles ## # Sets the map for which the tilesets should be displayed. ## def setMapDocument(self, mapDocument): if (self.mMapDocument == mapDocument): return # Hide while we update the tab bar, to avoid repeated layouting if sys.platform != 'darwin': self.widget().hide() self.setCurrentTiles(None) self.setCurrentTile(None) if (self.mMapDocument): # Remember the last visible tileset for this map tilesetName = self.mTabBar.tabText(self.mTabBar.currentIndex()) self.mCurrentTilesets.insert(self.mMapDocument, tilesetName) # Clear previous content while (self.mTabBar.count()): self.mTabBar.removeTab(0) while (self.mViewStack.count()): self.mViewStack.removeWidget(self.mViewStack.widget(0)) #self.mTilesets.clear() # Clear all connections to the previous document if (self.mMapDocument): self.mMapDocument.disconnect() self.mMapDocument = mapDocument if (self.mMapDocument): self.mTilesets = self.mMapDocument.map().tilesets() for tileset in self.mTilesets: view = TilesetView() view.setMapDocument(self.mMapDocument) view.setZoomable(self.mZoomable) self.mTabBar.addTab(tileset.name()) self.mViewStack.addWidget(view) self.mMapDocument.tilesetAdded.connect(self.tilesetAdded) self.mMapDocument.tilesetRemoved.connect(self.tilesetRemoved) self.mMapDocument.tilesetMoved.connect(self.tilesetMoved) self.mMapDocument.tilesetNameChanged.connect( self.tilesetNameChanged) self.mMapDocument.tilesetFileNameChanged.connect( self.updateActions) self.mMapDocument.tilesetChanged.connect(self.tilesetChanged) self.mMapDocument.tileAnimationChanged.connect( self.tileAnimationChanged) cacheName = self.mCurrentTilesets.take(self.mMapDocument) for i in range(self.mTabBar.count()): if (self.mTabBar.tabText(i) == cacheName): self.mTabBar.setCurrentIndex(i) break object = self.mMapDocument.currentObject() if object: if object.typeId() == Object.TileType: self.setCurrentTile(object) self.updateActions() if sys.platform != 'darwin': self.widget().show() ## # Synchronizes the selection with the given stamp. Ignored when the stamp is # changing because of a selection change in the TilesetDock. ## def selectTilesInStamp(self, stamp): if self.mEmittingStampCaptured: return processed = QSet() selections = QMap() for variation in stamp.variations(): tileLayer = variation.tileLayer() for cell in tileLayer: tile = cell.tile if tile: if (processed.contains(tile)): continue processed.insert(tile) # avoid spending time on duplicates tileset = tile.tileset() tilesetIndex = self.mTilesets.indexOf( tileset.sharedPointer()) if (tilesetIndex != -1): view = self.tilesetViewAt(tilesetIndex) if (not view.model()): # Lazily set up the model self.setupTilesetModel(view, tileset) model = view.tilesetModel() modelIndex = model.tileIndex(tile) selectionModel = view.selectionModel() _x = QItemSelection() _x.select(modelIndex, modelIndex) selections[selectionModel] = _x if (not selections.isEmpty()): self.mSynchronizingSelection = True # Mark captured tiles as selected for i in selections: selectionModel = i[0] selection = i[1] selectionModel.select(selection, QItemSelectionModel.SelectCurrent) # Show/edit properties of all captured tiles self.mMapDocument.setSelectedTiles(processed.toList()) # Update the current tile (useful for animation and collision editors) first = selections.first() selectionModel = first[0] selection = first[1] currentIndex = QModelIndex(selection.first().topLeft()) if (selectionModel.currentIndex() != currentIndex): selectionModel.setCurrentIndex(currentIndex, QItemSelectionModel.NoUpdate) else: self.currentChanged(currentIndex) self.mSynchronizingSelection = False def currentTilesetChanged(self): view = self.currentTilesetView() if view: s = view.selectionModel() if s: self.setCurrentTile(view.tilesetModel().tileAt( s.currentIndex())) ## # Returns the currently selected tile. ## def currentTile(self): return self.mCurrentTile def changeEvent(self, e): super().changeEvent(e) x = e.type() if x == QEvent.LanguageChange: self.retranslateUi() else: pass def dragEnterEvent(self, e): urls = e.mimeData().urls() if (not urls.isEmpty() and not urls.at(0).toLocalFile().isEmpty()): e.accept() def dropEvent(self, e): paths = QStringList() for url in e.mimeData().urls(): localFile = url.toLocalFile() if (not localFile.isEmpty()): paths.append(localFile) if (not paths.isEmpty()): self.tilesetsDropped.emit(paths) e.accept() def selectionChanged(self): self.updateActions() if not self.mSynchronizingSelection: self.updateCurrentTiles() def currentChanged(self, index): if (not index.isValid()): return model = index.model() self.setCurrentTile(model.tileAt(index)) def updateActions(self): external = False hasImageSource = False hasSelection = False view = None index = self.mTabBar.currentIndex() if (index > -1): view = self.tilesetViewAt(index) if (view): tileset = self.mTilesets.at(index) if (not view.model()): # Lazily set up the model self.setupTilesetModel(view, tileset) self.mViewStack.setCurrentIndex(index) external = tileset.isExternal() hasImageSource = tileset.imageSource() != '' hasSelection = view.selectionModel().hasSelection() tilesetIsDisplayed = view != None mapIsDisplayed = self.mMapDocument != None self.mNewTileset.setEnabled(mapIsDisplayed) self.mImportTileset.setEnabled(tilesetIsDisplayed and external) self.mExportTileset.setEnabled(tilesetIsDisplayed and not external) self.mPropertiesTileset.setEnabled(tilesetIsDisplayed and not external) self.mDeleteTileset.setEnabled(tilesetIsDisplayed) self.mEditTerrain.setEnabled(tilesetIsDisplayed and not external) self.mAddTiles.setEnabled(tilesetIsDisplayed and not hasImageSource and not external) self.mRemoveTiles.setEnabled(tilesetIsDisplayed and not hasImageSource and hasSelection and not external) def updateCurrentTiles(self): view = self.currentTilesetView() if (not view): return s = view.selectionModel() if (not s): return indexes = s.selection().indexes() if len(indexes) == 0: return first = indexes[0] minX = first.column() maxX = first.column() minY = first.row() maxY = first.row() for index in indexes: if minX > index.column(): minX = index.column() if maxX < index.column(): maxX = index.column() if minY > index.row(): minY = index.row() if maxY < index.row(): maxY = index.row() # Create a tile layer from the current selection tileLayer = TileLayer(QString(), 0, 0, maxX - minX + 1, maxY - minY + 1) model = view.tilesetModel() for index in indexes: tileLayer.setCell(index.column() - minX, index.row() - minY, Cell(model.tileAt(index))) self.setCurrentTiles(tileLayer) def indexPressed(self, index): view = self.currentTilesetView() tile = view.tilesetModel().tileAt(index) if tile: self.mMapDocument.setCurrentObject(tile) def tilesetAdded(self, index, tileset): view = TilesetView() view.setMapDocument(self.mMapDocument) view.setZoomable(self.mZoomable) self.mTilesets.insert(index, tileset.sharedPointer()) self.mTabBar.insertTab(index, tileset.name()) self.mViewStack.insertWidget(index, view) self.updateActions() def tilesetChanged(self, tileset): # Update the affected tileset model, if it exists index = indexOf(self.mTilesets, tileset) if (index < 0): return model = self.tilesetViewAt(index).tilesetModel() if model: model.tilesetChanged() def tilesetRemoved(self, tileset): # Delete the related tileset view index = indexOf(self.mTilesets, tileset) self.mTilesets.removeAt(index) self.mTabBar.removeTab(index) self.tilesetViewAt(index).close() # Make sure we don't reference this tileset anymore if (self.mCurrentTiles): # TODO: Don't clean unnecessarily (but first the concept of # "current brush" would need to be introduced) cleaned = self.mCurrentTiles.clone() cleaned.removeReferencesToTileset(tileset) self.setCurrentTiles(cleaned) if (self.mCurrentTile and self.mCurrentTile.tileset() == tileset): self.setCurrentTile(None) self.updateActions() def tilesetMoved(self, _from, to): self.mTilesets.insert(to, self.mTilesets.takeAt(_from)) # Move the related tileset views widget = self.mViewStack.widget(_from) self.mViewStack.removeWidget(widget) self.mViewStack.insertWidget(to, widget) self.mViewStack.setCurrentIndex(self.mTabBar.currentIndex()) # Update the titles of the affected tabs start = min(_from, to) end = max(_from, to) for i in range(start, end + 1): tileset = self.mTilesets.at(i) if (self.mTabBar.tabText(i) != tileset.name()): self.mTabBar.setTabText(i, tileset.name()) def tilesetNameChanged(self, tileset): index = indexOf(self.mTilesets, tileset) self.mTabBar.setTabText(index, tileset.name()) def tileAnimationChanged(self, tile): view = self.currentTilesetView() if view: model = view.tilesetModel() if model: model.tileChanged(tile) ## # Removes the currently selected tileset. ## def removeTileset(self, *args): l = len(args) if l == 0: currentIndex = self.mViewStack.currentIndex() if (currentIndex != -1): self.removeTileset(self.mViewStack.currentIndex()) elif l == 1: ## # Removes the tileset at the given index. Prompting the user when the tileset # is in use by the map. ## index = args[0] tileset = self.mTilesets.at(index).data() inUse = self.mMapDocument.map().isTilesetUsed(tileset) # If the tileset is in use, warn the user and confirm removal if (inUse): warning = QMessageBox( QMessageBox.Warning, self.tr("Remove Tileset"), self.tr("The tileset \"%s\" is still in use by the map!" % tileset.name()), QMessageBox.Yes | QMessageBox.No, self) warning.setDefaultButton(QMessageBox.Yes) warning.setInformativeText( self.tr("Remove this tileset and all references " "to the tiles in this tileset?")) if (warning.exec() != QMessageBox.Yes): return remove = RemoveTileset(self.mMapDocument, index, tileset) undoStack = self.mMapDocument.undoStack() if (inUse): # Remove references to tiles in this tileset from the current map def referencesTileset(cell): tile = cell.tile if tile: return tile.tileset() == tileset return False undoStack.beginMacro(remove.text()) removeTileReferences(self.mMapDocument, referencesTileset) undoStack.push(remove) if (inUse): undoStack.endMacro() def moveTileset(self, _from, to): command = MoveTileset(self.mMapDocument, _from, to) self.mMapDocument.undoStack().push(command) def editTilesetProperties(self): tileset = self.currentTileset() if (not tileset): return self.mMapDocument.setCurrentObject(tileset) self.mMapDocument.emitEditCurrentObject() def importTileset(self): tileset = self.currentTileset() if (not tileset): return command = SetTilesetFileName(self.mMapDocument, tileset, QString()) self.mMapDocument.undoStack().push(command) def exportTileset(self): tileset = self.currentTileset() if (not tileset): return tsxFilter = self.tr("Tiled tileset files (*.tsx)") helper = FormatHelper(FileFormat.ReadWrite, tsxFilter) prefs = preferences.Preferences.instance() suggestedFileName = prefs.lastPath( preferences.Preferences.ExternalTileset) suggestedFileName += '/' suggestedFileName += tileset.name() extension = ".tsx" if (not suggestedFileName.endswith(extension)): suggestedFileName += extension selectedFilter = tsxFilter fileName, _ = QFileDialog.getSaveFileName(self, self.tr("Export Tileset"), suggestedFileName, helper.filter(), selectedFilter) if fileName == '': return prefs.setLastPath(preferences.Preferences.ExternalTileset, QFileInfo(fileName).path()) tsxFormat = TsxTilesetFormat() format = helper.formatByNameFilter(selectedFilter) if not format: format = tsxFormat if format.write(tileset, fileName): command = SetTilesetFileName(self.mMapDocument, tileset, fileName) self.mMapDocument.undoStack().push(command) else: error = format.errorString() QMessageBox.critical(self.window(), self.tr("Export Tileset"), self.tr("Error saving tileset: %s" % error)) def editTerrain(self): tileset = self.currentTileset() if (not tileset): return editTerrainDialog = EditTerrainDialog(self.mMapDocument, tileset, self) editTerrainDialog.exec() def addTiles(self): tileset = self.currentTileset() if (not tileset): return prefs = preferences.Preferences.instance() startLocation = QFileInfo( prefs.lastPath(preferences.Preferences.ImageFile)).absolutePath() filter = Utils.readableImageFormatsFilter() files = QFileDialog.getOpenFileNames(self.window(), self.tr("Add Tiles"), startLocation, filter) tiles = QList() id = tileset.tileCount() for file in files: image = QPixmap(file) if (not image.isNull()): tiles.append(Tile(image, file, id, tileset)) id += 1 else: warning = QMessageBox(QMessageBox.Warning, self.tr("Add Tiles"), self.tr("Could not load \"%s\"!" % file), QMessageBox.Ignore | QMessageBox.Cancel, self.window()) warning.setDefaultButton(QMessageBox.Ignore) if (warning.exec() != QMessageBox.Ignore): tiles.clear() return if (tiles.isEmpty()): return prefs.setLastPath(preferences.Preferences.ImageFile, files.last()) self.mMapDocument.undoStack().push( AddTiles(self.mMapDocument, tileset, tiles)) def removeTiles(self): view = self.currentTilesetView() if (not view): return if (not view.selectionModel().hasSelection()): return indexes = view.selectionModel().selectedIndexes() model = view.tilesetModel() tileIds = RangeSet() tiles = QList() for index in indexes: tile = model.tileAt(index) if tile: tileIds.insert(tile.id()) tiles.append(tile) def matchesAnyTile(cell): tile = cell.tile if tile: return tiles.contains(tile) return False inUse = self.hasTileReferences(self.mMapDocument, matchesAnyTile) # If the tileset is in use, warn the user and confirm removal if (inUse): warning = QMessageBox( QMessageBox.Warning, self.tr("Remove Tiles"), self.tr("One or more of the tiles to be removed are " "still in use by the map!"), QMessageBox.Yes | QMessageBox.No, self) warning.setDefaultButton(QMessageBox.Yes) warning.setInformativeText( self.tr("Remove all references to these tiles?")) if (warning.exec() != QMessageBox.Yes): return undoStack = self.mMapDocument.undoStack() undoStack.beginMacro(self.tr("Remove Tiles")) removeTileReferences(self.mMapDocument, matchesAnyTile) # Iterate backwards over the ranges in order to keep the indexes valid firstRange = tileIds.begin() it = tileIds.end() if (it == firstRange): # no range return tileset = view.tilesetModel().tileset() while (it != firstRange): it -= 1 item = tileIds.item(it) length = item[1] - item[0] + 1 undoStack.push( RemoveTiles(self.mMapDocument, tileset, item[0], length)) undoStack.endMacro() # Clear the current tiles, will be referencing the removed tiles self.setCurrentTiles(None) self.setCurrentTile(None) def documentAboutToClose(self, mapDocument): self.mCurrentTilesets.remove(mapDocument) def refreshTilesetMenu(self): self.mTilesetMenu.clear() if (self.mTilesetMenuMapper): self.mTabBar.disconnect(self.mTilesetMenuMapper) del self.mTilesetMenuMapper self.mTilesetMenuMapper = QSignalMapper(self) self.mTilesetMenuMapper.mapped.connect(self.mTabBar.setCurrentIndex) currentIndex = self.mTabBar.currentIndex() for i in range(self.mTabBar.count()): action = QAction(self.mTabBar.tabText(i), self) action.setCheckable(True) self.mTilesetActionGroup.addAction(action) if (i == currentIndex): action.setChecked(True) self.mTilesetMenu.addAction(action) action.triggered.connect(self.mTilesetMenuMapper.map) self.mTilesetMenuMapper.setMapping(action, i) def setCurrentTile(self, tile): if (self.mCurrentTile == tile): return self.mCurrentTile = tile self.currentTileChanged.emit([tile]) if (tile): self.mMapDocument.setCurrentObject(tile) def setCurrentTiles(self, tiles): if (self.mCurrentTiles == tiles): return del self.mCurrentTiles self.mCurrentTiles = tiles # Set the selected tiles on the map document if (tiles): selectedTiles = QList() for y in range(tiles.height()): for x in range(tiles.width()): cell = tiles.cellAt(x, y) if (not cell.isEmpty()): selectedTiles.append(cell.tile) self.mMapDocument.setSelectedTiles(selectedTiles) # Create a tile stamp with these tiles map = self.mMapDocument.map() stamp = Map(map.orientation(), tiles.width(), tiles.height(), map.tileWidth(), map.tileHeight()) stamp.addLayer(tiles.clone()) stamp.addTilesets(tiles.usedTilesets()) self.mEmittingStampCaptured = True self.stampCaptured.emit(TileStamp(stamp)) self.mEmittingStampCaptured = False def retranslateUi(self): self.setWindowTitle(self.tr("Tilesets")) self.mNewTileset.setText(self.tr("New Tileset")) self.mImportTileset.setText(self.tr("Import Tileset")) self.mExportTileset.setText(self.tr("Export Tileset As...")) self.mPropertiesTileset.setText(self.tr("Tileset Properties")) self.mDeleteTileset.setText(self.tr("Remove Tileset")) self.mEditTerrain.setText(self.tr("Edit Terrain Information")) self.mAddTiles.setText(self.tr("Add Tiles")) self.mRemoveTiles.setText(self.tr("Remove Tiles")) def currentTileset(self): index = self.mTabBar.currentIndex() if (index == -1): return None return self.mTilesets.at(index) def currentTilesetView(self): return self.mViewStack.currentWidget() def tilesetViewAt(self, index): return self.mViewStack.widget(index) def setupTilesetModel(self, view, tileset): view.setModel(TilesetModel(tileset, view)) s = view.selectionModel() s.selectionChanged.connect(self.selectionChanged) s.currentChanged.connect(self.currentChanged) view.pressed.connect(self.indexPressed)
class MainWindow(QWidget): def __init__(self, title="Raven"): super().__init__() # self.window_width = 350 # self.window_height = 600 # self.setFixedSize(self.window_width, self.window_height) self.showMaximized() self.setWindowTitle(title) self.init_ui() def init_ui(self): bottom_bar = BottomWidget(self) bottom_bar.home_click_listener.connect(self.on_home_click) bottom_bar.logout_click_listener.connect(self.on_logout_click) self.fragment = QStackedWidget() layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.fragment) bottom_bar.on_back_press_listener.connect(self.on_back_press) layout.addWidget(bottom_bar) self.setLayout(layout) # self.add_screen(LoginWidget("Login Screen")) # self.add_screen(CameraWidget()) self.add_screen(AttendenceOptionWidget()) def add_screen(self, screen: base_widget): screen.add_screen_listener.connect(self.add_screen) screen.on_back_press_listener.connect(self.on_back_press) self.fragment.addWidget(screen) self.fragment.setCurrentIndex(self.fragment.count() - 1) self.updateUi() def on_back_press(self): if self.fragment.count() > 1: widget = self.fragment.currentWidget() self.fragment.removeWidget(widget) widget.deleteLater() self.updateUi() def updateUi(self): widget: base_widget = self.fragment.currentWidget() self.setWindowTitle(widget.title) def on_home_click(self): self.remove_all_screen() def remove_all_screen(self): while self.fragment.count() > 1: widget = self.fragment.currentWidget() self.fragment.removeWidget(widget) widget.deleteLater() self.updateUi() def on_logout_click(self): print("On Logout Click")
class DataSelectionGui(QWidget): """ Manages all GUI elements in the data selection applet. This class itself is the central widget and also owns/manages the applet drawer widgets. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget( self ): return self def appletDrawer( self ): return self._drawer def menus( self ): return [] def viewerControlWidget(self): return self._viewerControlWidgetStack def setImageIndex(self, imageIndex): if imageIndex is not None: self.laneSummaryTableView.selectRow(imageIndex) for detailWidget in self._detailViewerWidgets: detailWidget.selectRow(imageIndex) def stopAndCleanUp(self): self._cleaning_up = True for editor in list(self.volumeEditors.values()): self.viewerStack.removeWidget( editor ) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() ) editor.stopAndCleanUp() self.volumeEditors.clear() def imageLaneAdded(self, laneIndex): if len(self.laneSummaryTableView.selectedIndexes()) == 0: self.laneSummaryTableView.selectRow(laneIndex) # We don't have any real work to do because this gui initiated the lane addition in the first place if self.guiMode != GuiMode.Batch: if(len(self.topLevelOperator.DatasetGroup) != laneIndex+1): import warnings warnings.warn("DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.DatasetGroup), laneIndex)) def imageLaneRemoved(self, laneIndex, finalLength): # There's nothing to do here because the GUI already # handles operator resizes via slot callbacks. pass def allowLaneSelectionChange(self): return False ########################################### ########################################### class UserCancelledError(Exception): # This exception type is raised when the user cancels the # addition of dataset files in the middle of the process somewhere. # It isn't an error -- it's used for control flow. pass def __init__(self, parentApplet, dataSelectionOperator, serializer, instructionText, guiMode=GuiMode.Normal, max_lanes=None, show_axis_details=False): """ Constructor. :param dataSelectionOperator: The top-level operator. Must be of type :py:class:`OpMultiLaneDataSelectionGroup`. :param serializer: The applet's serializer. Must be of type :py:class:`DataSelectionSerializer` :param instructionText: A string to display in the applet drawer. :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``. Currently, there is no difference between normal and batch mode. :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow. If ``None``, there is no maximum. """ super(DataSelectionGui, self).__init__() self._cleaning_up = False self.parentApplet = parentApplet self._max_lanes = max_lanes self._default_h5_volumes = {} self.show_axis_details = show_axis_details self._viewerControls = QWidget() self.topLevelOperator = dataSelectionOperator self.guiMode = guiMode self.serializer = serializer self.threadRouter = ThreadRouter(self) self._initCentralUic() self._initAppletDrawerUic(instructionText) self._viewerControlWidgetStack = QStackedWidget(self) def handleImageRemove(multislot, index, finalLength): # Remove the viewer for this dataset datasetSlot = self.topLevelOperator.DatasetGroup[index] if datasetSlot in list(self.volumeEditors.keys()): editor = self.volumeEditors[datasetSlot] self.viewerStack.removeWidget( editor ) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() ) editor.stopAndCleanUp() self.topLevelOperator.DatasetGroup.notifyRemove( bind( handleImageRemove ) ) opWorkflow = self.topLevelOperator.parent assert hasattr(opWorkflow.shell, 'onSaveProjectActionTriggered'), \ "This class uses the IlastikShell.onSaveProjectActionTriggered function. Did you rename it?" def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0]+'/' uic.loadUi(localDir+"/dataSelection.ui", self) self._initTableViews() self._initViewerStack() self.splitter.setSizes( [150, 850] ) def _initAppletDrawerUic(self, instructionText): """ Load the ui file for the applet drawer, which we own. """ localDir = os.path.split(__file__)[0]+'/' self._drawer = uic.loadUi(localDir+"/dataSelectionDrawer.ui") self._drawer.instructionLabel.setText( instructionText ) def _initTableViews(self): self.fileInfoTabWidget.setTabText( 0, "Summary" ) self.laneSummaryTableView.setModel( DataLaneSummaryTableModel(self, self.topLevelOperator) ) self.laneSummaryTableView.dataLaneSelected.connect( self.showDataset ) self.laneSummaryTableView.addFilesRequested.connect( self.addFiles ) self.laneSummaryTableView.addStackRequested.connect( self.addStack ) self.laneSummaryTableView.removeLanesRequested.connect( self.handleRemoveLaneButtonClicked ) # These two helper functions enable/disable an 'add files' button for a given role # based on the the max lane index for that role and the overall permitted max_lanes def _update_button_status(viewer, role_index): if self._max_lanes: viewer.setEnabled( self._findFirstEmptyLane(role_index) < self._max_lanes ) def _handle_lane_added( button, role_index, lane_slot, lane_index ): def _handle_role_slot_added( role_slot, added_slot_index, *args ): if added_slot_index == role_index: role_slot.notifyReady( bind(_update_button_status, button, role_index) ) role_slot.notifyUnready( bind(_update_button_status, button, role_index) ) lane_slot[lane_index].notifyInserted( _handle_role_slot_added ) self._retained = [] # Retain menus so they don't get deleted self._detailViewerWidgets = [] for roleIndex, role in enumerate(self.topLevelOperator.DatasetRoles.value): detailViewer = DatasetDetailedInfoTableView(self) detailViewer.setModel(DatasetDetailedInfoTableModel(self, self.topLevelOperator, roleIndex)) self._detailViewerWidgets.append( detailViewer ) # Button detailViewer.addFilesRequested.connect( partial(self.addFiles, roleIndex)) detailViewer.addStackRequested.connect( partial(self.addStack, roleIndex)) detailViewer.addRemoteVolumeRequested.connect( partial(self.addDvidVolume, roleIndex)) # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, detailViewer, roleIndex ) ) self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, detailViewer, roleIndex ) ) # While we're at it, do the same for the buttons in the summary table, too self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) ) self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) ) # Context menu detailViewer.replaceWithFileRequested.connect( partial(self.handleReplaceFile, roleIndex) ) detailViewer.replaceWithStackRequested.connect( partial(self.addStack, roleIndex) ) detailViewer.editRequested.connect( partial(self.editDatasetInfo, roleIndex) ) detailViewer.resetRequested.connect( partial(self.handleClearDatasets, roleIndex) ) # Drag-and-drop detailViewer.addFilesRequestedDrop.connect( partial( self.addFileNames, roleIndex=roleIndex ) ) # Selection handling def showFirstSelectedDataset( _roleIndex, lanes ): if lanes: self.showDataset( lanes[0], _roleIndex ) detailViewer.dataLaneSelected.connect( partial(showFirstSelectedDataset, roleIndex) ) self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role) self.fileInfoTabWidget.currentChanged.connect( self.handleSwitchTabs ) self.fileInfoTabWidget.setCurrentIndex(0) def handleSwitchTabs(self, tabIndex ): if tabIndex < len(self._detailViewerWidgets): roleIndex = tabIndex # If summary tab is moved to the front, change this line. detailViewer = self._detailViewerWidgets[roleIndex] selectedLanes = detailViewer.selectedLanes if selectedLanes: self.showDataset( selectedLanes[0], roleIndex ) def _initViewerStack(self): self.volumeEditors = {} self.viewerStack.addWidget( QWidget() ) def handleRemoveLaneButtonClicked(self): """ The user clicked the "Remove" button. Remove the currently selected row(s) from both the GUI and the top-level operator. """ # Figure out which lanes to remove selectedIndexes = self.laneSummaryTableView.selectedIndexes() rows = set() for modelIndex in selectedIndexes: rows.add( modelIndex.row() ) # Don't remove the last row, which is just buttons. rows.discard( self.laneSummaryTableView.model().rowCount()-1 ) # Remove in reverse order so row numbers remain consistent for row in reversed(sorted(rows)): # Remove lanes from the operator. # The table model will notice the changes and update the rows accordingly. finalSize = len(self.topLevelOperator.DatasetGroup) - 1 self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize) @threadRouted def showDataset(self, laneIndex, roleIndex=None): if self._cleaning_up: return if laneIndex == -1: self.viewerStack.setCurrentIndex(0) return assert threading.current_thread().name == "MainThread" if laneIndex >= len(self.topLevelOperator.DatasetGroup): return datasetSlot = self.topLevelOperator.DatasetGroup[laneIndex] # Create if necessary if datasetSlot not in list(self.volumeEditors.keys()): class DatasetViewer(LayerViewerGui): def moveToTop(self, roleIndex): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return datasetRoles = opLaneView.DatasetRoles.value if roleIndex >= len(datasetRoles): return roleName = datasetRoles[roleIndex] try: layerIndex = [l.name for l in self.layerstack].index(roleName) except ValueError: return else: self.layerstack.selectRow(layerIndex) self.layerstack.moveSelectedToTop() def setupLayers(self): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return [] layers = [] datasetRoles = opLaneView.DatasetRoles.value for roleIndex, slot in enumerate(opLaneView.ImageGroup): if slot.ready(): roleName = datasetRoles[roleIndex] layer = self.createStandardLayerFromSlot(slot) layer.name = roleName layers.append(layer) return layers opLaneView = self.topLevelOperator.getLane(laneIndex) layerViewer = DatasetViewer(self.parentApplet, opLaneView, crosshair=False) # Maximize the x-y view by default. layerViewer.volumeEditorWidget.quadview.ensureMaximized(2) self.volumeEditors[datasetSlot] = layerViewer self.viewerStack.addWidget( layerViewer ) self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget() ) # Show the right one viewer = self.volumeEditors[datasetSlot] displayedRole = self.fileInfoTabWidget.currentIndex() viewer.moveToTop(displayedRole) self.viewerStack.setCurrentWidget( viewer ) self._viewerControlWidgetStack.setCurrentWidget( viewer.viewerControlWidget() ) def handleReplaceFile(self, roleIndex, startingLane): self.addFiles(roleIndex, startingLane) def addFiles(self, roleIndex, startingLane=None): """ The user clicked the "Add File" button. Ask him to choose a file (or several) and add them to both the GUI table and the top-level operator inputs. """ # Find the directory of the most recently opened image file mostRecentImageFile = PreferencesManager().get( 'DataSelection', 'recent image' ) mostRecentImageFile = str(mostRecentImageFile) if mostRecentImageFile is not None: defaultDirectory = os.path.split(mostRecentImageFile)[0] else: defaultDirectory = os.path.expanduser('~') # Launch the "Open File" dialog fileNames = self.getImageFileNamesToOpen(self, defaultDirectory) # If the user didn't cancel if len(fileNames) > 0: PreferencesManager().set('DataSelection', 'recent image', fileNames[0]) try: self.addFileNames(fileNames, roleIndex, startingLane) except Exception as ex: log_exception( logger ) QMessageBox.critical(self, "Error loading file", str(ex)) @classmethod def getImageFileNamesToOpen(cls, parent_window, defaultDirectory): """ Launch an "Open File" dialog to ask the user for one or more image files. """ extensions = OpDataSelection.SupportedExtensions filter_strs = ["*." + x for x in extensions] filters = ["{filt} ({filt})".format(filt=x) for x in filter_strs] filt_all_str = "Image files (" + ' '.join(filter_strs) + ')' fileNames = [] if ilastik_config.getboolean("ilastik", "debug"): # use Qt dialog in debug mode (more portable?) file_dialog = QFileDialog(parent_window, "Select Images") file_dialog.setOption(QFileDialog.DontUseNativeDialog, True) # do not display file types associated with a filter # the line for "Image files" is too long otherwise file_dialog.setNameFilters([filt_all_str] + filters) #file_dialog.setNameFilterDetailsVisible(False) # select multiple files file_dialog.setFileMode(QFileDialog.ExistingFiles) file_dialog.setDirectory( defaultDirectory ) if file_dialog.exec_(): fileNames = file_dialog.selectedFiles() else: # otherwise, use native dialog of the present platform fileNames, _filter = QFileDialog.getOpenFileNames(parent_window, "Select Images", defaultDirectory, filt_all_str) return fileNames def _findFirstEmptyLane(self, roleIndex): opTop = self.topLevelOperator # Determine the number of files this role already has # Search for the last valid value. firstNewLane = 0 for laneIndex, slot in reversed(list(zip(list(range(len(opTop.DatasetGroup))), opTop.DatasetGroup))): if slot[roleIndex].ready(): firstNewLane = laneIndex+1 break return firstNewLane def addFileNames(self, fileNames, roleIndex, startingLane=None, rois=None): """ Add the given filenames to both the GUI table and the top-level operator inputs. If startingLane is None, the filenames will be *appended* to the role's list of files. If rois is provided, it must be a list of (start,stop) tuples (one for each fileName) """ # What lanes will we touch? startingLane, endingLane = self._determineLaneRange(fileNames, roleIndex, startingLane) if startingLane is None: # Something went wrong. return # If we're only adding new lanes, NOT modifying existing lanes... adding_only = startingLane == len(self.topLevelOperator) # Create a list of DatasetInfos try: infos = self._createDatasetInfos(roleIndex, fileNames, rois) except DataSelectionGui.UserCancelledError: return # If no exception was thrown so far, set up the operator now loaded_all = self._configureOpWithInfos(roleIndex, startingLane, endingLane, infos) if loaded_all: # Now check the resulting slots. # If they should be copied to the project file, say so. self._reconfigureDatasetLocations(roleIndex, startingLane, endingLane) self._checkDataFormatWarnings(roleIndex, startingLane, endingLane) # If we succeeded in adding all images, show the first one. self.showDataset(startingLane, roleIndex) # Notify the workflow that we just added some new lanes. if adding_only: workflow = self.parentApplet.topLevelOperator.parent workflow.handleNewLanesAdded() # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() self.updateInternalPathVisiblity() def _determineLaneRange(self, fileNames, roleIndex, startingLane=None): """ Determine which lanes should be configured if the user wants to add the given fileNames to the specified role, starting at startingLane. If startingLane is None, assume the user wants to APPEND the files to the role's slots. """ if startingLane is None or startingLane == -1: startingLane = len(self.topLevelOperator.DatasetGroup) endingLane = startingLane+len(fileNames)-1 else: assert startingLane < len(self.topLevelOperator.DatasetGroup) max_files = len(self.topLevelOperator.DatasetGroup) - \ startingLane if len(fileNames) > max_files: msg = "You selected {num_selected} files for {num_slots} "\ "slots. To add new files use the 'Add new...' option "\ "in the context menu or the button in the last row."\ .format(num_selected=len(fileNames), num_slots=max_files) QMessageBox.critical( self, "Too many files", msg ) return (None, None) endingLane = min(startingLane+len(fileNames)-1, len(self.topLevelOperator.DatasetGroup)) if self._max_lanes and endingLane >= self._max_lanes: msg = "You may not add more than {} file(s) to this workflow. Please try again.".format( self._max_lanes ) QMessageBox.critical( self, "Too many files", msg ) return (None, None) return (startingLane, endingLane) def _createDatasetInfos(self, roleIndex, filePaths, rois): """ Create a list of DatasetInfos for the given filePaths and rois rois may be None, in which case it is ignored. """ if rois is None: rois = [None]*len(filePaths) assert len(rois) == len(filePaths) infos = [] for filePath, roi in zip(filePaths, rois): info = self._createDatasetInfo(roleIndex, filePath, roi) infos.append(info) return infos def _createDatasetInfo(self, roleIndex, filePath, roi): """ Create a DatasetInfo object for the given filePath and roi. roi may be None, in which case it is ignored. """ cwd = self.topLevelOperator.WorkingDirectory.value datasetInfo = DatasetInfo(filePath, cwd=cwd) datasetInfo.subvolume_roi = roi # (might be None) absPath, relPath = getPathVariants(filePath, cwd) # If the file is in a totally different tree from the cwd, # then leave the path as absolute. Otherwise, override with the relative path. if relPath is not None and len(os.path.commonprefix([cwd, absPath])) > 1: datasetInfo.filePath = relPath h5Exts = ['.ilp', '.h5', '.hdf5'] if os.path.splitext(datasetInfo.filePath)[1] in h5Exts: datasetNames = self.getPossibleInternalPaths( absPath ) if len(datasetNames) == 0: raise RuntimeError("HDF5 file %s has no image datasets" % datasetInfo.filePath) elif len(datasetNames) == 1: datasetInfo.filePath += str(datasetNames[0]) else: # If exactly one of the file's datasets matches a user's previous choice, use it. if roleIndex not in self._default_h5_volumes: self._default_h5_volumes[roleIndex] = set() previous_selections = self._default_h5_volumes[roleIndex] possible_auto_selections = previous_selections.intersection(datasetNames) if len(possible_auto_selections) == 1: datasetInfo.filePath += str(list(possible_auto_selections)[0]) else: # Ask the user which dataset to choose dlg = H5VolumeSelectionDlg(datasetNames, self) if dlg.exec_() == QDialog.Accepted: selected_index = dlg.combo.currentIndex() selected_dataset = str(datasetNames[selected_index]) datasetInfo.filePath += selected_dataset self._default_h5_volumes[roleIndex].add( selected_dataset ) else: raise DataSelectionGui.UserCancelledError() # Allow labels by default if this gui isn't being used for batch data. datasetInfo.allowLabels = ( self.guiMode == GuiMode.Normal ) return datasetInfo def _configureOpWithInfos(self, roleIndex, startingLane, endingLane, infos): """ Attempt to configure the specified role and lanes of the top-level operator with the given DatasetInfos. Returns True if all lanes were configured successfully, or False if something went wrong. """ opTop = self.topLevelOperator originalSize = len(opTop.DatasetGroup) # Resize the slot if necessary if len( opTop.DatasetGroup ) < endingLane+1: opTop.DatasetGroup.resize( endingLane+1 ) # Configure each subslot for laneIndex, info in zip(list(range(startingLane, endingLane+1)), infos): try: self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue( info ) except DatasetConstraintError as ex: return_val = [False] # Give the user a chance to fix the problem self.handleDatasetConstraintError(info, info.filePath, ex, roleIndex, laneIndex, return_val) if not return_val[0]: # Not successfully repaired. Roll back the changes and give up. opTop.DatasetGroup.resize( originalSize ) return False except OpDataSelection.InvalidDimensionalityError as ex: opTop.DatasetGroup.resize( originalSize ) QMessageBox.critical( self, "Dataset has different dimensionality", ex.message ) return False except Exception as ex: msg = "Wasn't able to load your dataset into the workflow. See error log for details." log_exception( logger, msg ) QMessageBox.critical( self, "Dataset Load Error", msg ) opTop.DatasetGroup.resize( originalSize ) return False return True def _reconfigureDatasetLocations(self, roleIndex, startingLane, endingLane): """ Check the metadata for the given slots. If the data is stored a format that is poorly optimized for 3D access, then configure it to be copied to the project file. Finally, save the project if we changed something. """ save_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane+1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.prefer_2d and 'z' in output_slot.meta.axistags: shape = numpy.array(output_slot.meta.shape) total_volume = numpy.prod(shape) # Only copy to the project file if the total volume is reasonably small if total_volume < 0.5e9: info_slot = opTop.DatasetGroup[lane_index][roleIndex] info = info_slot.value info.location = DatasetInfo.Location.ProjectInternal info_slot.setValue( info, check_changed=False ) save_needed = True if save_needed: logger.info("Some of your data cannot be accessed efficiently in 3D in its current format." " It will now be copied to the project file.") opWorkflow = self.topLevelOperator.parent opWorkflow.shell.onSaveProjectActionTriggered() def _checkDataFormatWarnings(self, roleIndex, startingLane, endingLane): warn_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane+1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.inefficient_format: warn_needed = True if warn_needed: QMessageBox.warning( self, "Inefficient Data Format", "Your data cannot be accessed efficiently in its current format. " "Check the console output for details.\n" "(For HDF5 files, be sure to enable chunking on your dataset.)" ) @threadRouted def handleDatasetConstraintError(self, info, filename, ex, roleIndex, laneIndex, return_val=[False]): if ex.unfixable: msg = ( "Can't use dataset:\n\n" + filename + "\n\n" + "because it violates a constraint of the {} component.\n\n".format( ex.appletName ) + ex.message + "\n\n" ) QMessageBox.warning( self, "Can't use dataset", msg ) return_val[0] = False else: msg = ( "Can't use default properties for dataset:\n\n" + filename + "\n\n" + "because it violates a constraint of the {} component.\n\n".format( ex.appletName ) + ex.message + "\n\n" + "If possible, fix this problem by adjusting the dataset properties in the next window, or hit 'cancel' to abort." ) QMessageBox.warning( self, "Dataset Needs Correction", msg ) # The success of this is 'returned' via our special out-param # (We can't return a value from this func because it is @threadRouted. successfully_repaired = self.repairDatasetInfo( info, roleIndex, laneIndex ) return_val[0] = successfully_repaired def repairDatasetInfo(self, info, roleIndex, laneIndex): """Open the dataset properties editor and return True if the new properties are acceptable.""" defaultInfos = {} defaultInfos[laneIndex] = info editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, [laneIndex], defaultInfos, show_axis_details=self.show_axis_details) dlg_state = editorDlg.exec_() return ( dlg_state == QDialog.Accepted ) @classmethod def getPossibleInternalPaths(cls, absPath, min_ndim=2, max_ndim=5): datasetNames = [] # Open the file as a read-only so we can get a list of the internal paths with h5py.File(absPath, 'r') as f: # Define a closure to collect all of the dataset names in the file. def accumulateDatasetPaths(name, val): if type(val) == h5py._hl.dataset.Dataset and min_ndim <= len(val.shape) <= max_ndim: datasetNames.append( '/' + name ) # Visit every group/dataset in the file f.visititems(accumulateDatasetPaths) return datasetNames def addStack(self, roleIndex, laneIndex): """ The user clicked the "Import Stack Files" button. """ stackDlg = StackFileSelectionWidget(self) stackDlg.exec_() if stackDlg.result() != QDialog.Accepted : return files = stackDlg.selectedFiles sequence_axis = stackDlg.sequence_axis if len(files) == 0: return cwd = self.topLevelOperator.WorkingDirectory.value info = DatasetInfo(os.path.pathsep.join(files), cwd=cwd) originalNumLanes = len(self.topLevelOperator.DatasetGroup) if laneIndex is None or laneIndex == -1: laneIndex = len(self.topLevelOperator.DatasetGroup) if len(self.topLevelOperator.DatasetGroup) < laneIndex+1: self.topLevelOperator.DatasetGroup.resize(laneIndex+1) def importStack(): self.parentApplet.busy = True self.parentApplet.appletStateUpdateRequested() # Serializer will update the operator for us, which will propagate to the GUI. try: self.serializer.importStackAsLocalDataset( info, sequence_axis ) try: self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue(info) except DatasetConstraintError as ex: # Give the user a chance to repair the problem. filename = files[0] + "\n...\n" + files[-1] return_val = [False] self.handleDatasetConstraintError( info, filename, ex, roleIndex, laneIndex, return_val ) if not return_val[0]: # Not successfully repaired. Roll back the changes and give up. self.topLevelOperator.DatasetGroup.resize(originalNumLanes) finally: self.parentApplet.busy = False self.parentApplet.appletStateUpdateRequested() req = Request( importStack ) req.notify_finished( lambda result: self.showDataset(laneIndex, roleIndex) ) req.notify_failed( partial(self.handleFailedStackLoad, files, originalNumLanes ) ) req.submit() @threadRouted def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info): msg = "Failed to load stack due to the following error:\n{}".format( exc ) msg += "\nAttempted stack files were:\n" msg += "\n".join(files) log_exception( logger, msg, exc_info ) QMessageBox.critical(self, "Failed to load image stack", msg) self.topLevelOperator.DatasetGroup.resize(originalNumLanes) def handleClearDatasets(self, roleIndex, selectedRows): for row in selectedRows: self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect() # Remove all operators that no longer have any connected slots laneIndexes = list(range( len(self.topLevelOperator.DatasetGroup))) for laneIndex, multislot in reversed(list(zip(laneIndexes, self.topLevelOperator.DatasetGroup))): any_ready = False for slot in multislot: any_ready |= slot.ready() if not any_ready: self.topLevelOperator.DatasetGroup.removeSlot( laneIndex, len(self.topLevelOperator.DatasetGroup)-1 ) # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() def editDatasetInfo(self, roleIndex, laneIndexes): editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, laneIndexes, show_axis_details=self.show_axis_details) editorDlg.exec_() self.parentApplet.appletStateUpdateRequested() def updateInternalPathVisiblity(self): for view in self._detailViewerWidgets: model = view.model() view.setColumnHidden(DatasetDetailedInfoColumn.InternalID, not model.hasInternalPaths()) def addDvidVolume(self, roleIndex, laneIndex): recent_hosts_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Hosts") recent_hosts = recent_hosts_pref.get() if not recent_hosts: recent_hosts = ["localhost:8000"] recent_hosts = [h for h in recent_hosts if h] # There used to be a bug where empty strings could be saved. Filter those out. recent_nodes_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Nodes") recent_nodes = recent_nodes_pref.get() or {} from .dvidDataSelectionBrowser import DvidDataSelectionBrowser browser = DvidDataSelectionBrowser(recent_hosts, recent_nodes, parent=self) if browser.exec_() == DvidDataSelectionBrowser.Rejected: return if None in browser.get_selection(): QMessageBox.critical("Couldn't use your selection.") return rois = None hostname, repo_uuid, volume_name, node_uuid, typename = browser.get_selection() dvid_url = 'http://{hostname}/api/node/{node_uuid}/{volume_name}'.format( **locals() ) subvolume_roi = browser.get_subvolume_roi() # Relocate host to top of 'recent' list, and limit list to 10 items. try: i = recent_hosts.index(hostname) del recent_hosts[i] except ValueError: pass finally: recent_hosts.insert(0, hostname) recent_hosts = recent_hosts[:10] # Save pref recent_hosts_pref.set(recent_hosts) recent_nodes[hostname] = node_uuid recent_nodes_pref.set(recent_nodes) if subvolume_roi is None: self.addFileNames([dvid_url], roleIndex, laneIndex) else: start, stop = subvolume_roi self.addFileNames([dvid_url], roleIndex, laneIndex, [(start, stop)])
class E5SideBar(QWidget): """ Class implementing a sidebar with a widget area, that is hidden or shown, if the current tab is clicked again. """ Version = 1 North = 0 East = 1 South = 2 West = 3 def __init__(self, orientation=None, delay=200, parent=None): """ Constructor @param orientation orientation of the sidebar widget (North, East, South, West) @param delay value for the expand/shrink delay in milliseconds (integer) @param parent parent widget (QWidget) """ super(E5SideBar, self).__init__(parent) self.__tabBar = QTabBar() self.__tabBar.setDrawBase(True) self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setUsesScrollButtons(True) self.__tabBar.setDrawBase(False) self.__stackedWidget = QStackedWidget(self) self.__stackedWidget.setContentsMargins(0, 0, 0, 0) self.__autoHideButton = QToolButton() self.__autoHideButton.setCheckable(True) self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) self.__autoHideButton.setChecked(True) self.__autoHideButton.setToolTip( self.tr("Deselect to activate automatic collapsing")) self.barLayout = QBoxLayout(QBoxLayout.LeftToRight) self.barLayout.setContentsMargins(0, 0, 0, 0) self.layout = QBoxLayout(QBoxLayout.TopToBottom) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.barLayout.addWidget(self.__autoHideButton) self.barLayout.addWidget(self.__tabBar) self.layout.addLayout(self.barLayout) self.layout.addWidget(self.__stackedWidget) self.setLayout(self.layout) # initialize the delay timer self.__actionMethod = None self.__delayTimer = QTimer(self) self.__delayTimer.setSingleShot(True) self.__delayTimer.setInterval(delay) self.__delayTimer.timeout.connect(self.__delayedAction) self.__minimized = False self.__minSize = 0 self.__maxSize = 0 self.__bigSize = QSize() self.splitter = None self.splitterSizes = [] self.__hasFocus = False # flag storing if this widget or any child has the focus self.__autoHide = False self.__tabBar.installEventFilter(self) self.__orientation = E5SideBar.North if orientation is None: orientation = E5SideBar.North self.setOrientation(orientation) self.__tabBar.currentChanged[int].connect( self.__stackedWidget.setCurrentIndex) e5App().focusChanged[QWidget, QWidget].connect(self.__appFocusChanged) self.__autoHideButton.toggled[bool].connect(self.__autoHideToggled) def setSplitter(self, splitter): """ Public method to set the splitter managing the sidebar. @param splitter reference to the splitter (QSplitter) """ self.splitter = splitter self.splitter.splitterMoved.connect(self.__splitterMoved) self.splitter.setChildrenCollapsible(False) index = self.splitter.indexOf(self) self.splitter.setCollapsible(index, False) def __splitterMoved(self, pos, index): """ Private slot to react on splitter moves. @param pos new position of the splitter handle (integer) @param index index of the splitter handle (integer) """ if self.splitter: self.splitterSizes = self.splitter.sizes() def __delayedAction(self): """ Private slot to handle the firing of the delay timer. """ if self.__actionMethod is not None: self.__actionMethod() def setDelay(self, delay): """ Public method to set the delay value for the expand/shrink delay in milliseconds. @param delay value for the expand/shrink delay in milliseconds (integer) """ self.__delayTimer.setInterval(delay) def delay(self): """ Public method to get the delay value for the expand/shrink delay in milliseconds. @return value for the expand/shrink delay in milliseconds (integer) """ return self.__delayTimer.interval() def __cancelDelayTimer(self): """ Private method to cancel the current delay timer. """ self.__delayTimer.stop() self.__actionMethod = None def shrink(self): """ Public method to record a shrink request. """ self.__delayTimer.stop() self.__actionMethod = self.__shrinkIt self.__delayTimer.start() def __shrinkIt(self): """ Private method to shrink the sidebar. """ self.__minimized = True self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() if self.splitter: self.splitterSizes = self.splitter.sizes() self.__stackedWidget.hide() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.setFixedHeight(self.__tabBar.minimumSizeHint().height()) else: self.setFixedWidth(self.__tabBar.minimumSizeHint().width()) self.__actionMethod = None def expand(self): """ Public method to record a expand request. """ self.__delayTimer.stop() self.__actionMethod = self.__expandIt self.__delayTimer.start() def __expandIt(self): """ Private method to expand the sidebar. """ self.__minimized = False self.__stackedWidget.show() self.resize(self.__bigSize) if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = max(self.__minSize, self.minimumSizeHint().height()) self.setMinimumHeight(minSize) self.setMaximumHeight(self.__maxSize) else: minSize = max(self.__minSize, self.minimumSizeHint().width()) self.setMinimumWidth(minSize) self.setMaximumWidth(self.__maxSize) if self.splitter: self.splitter.setSizes(self.splitterSizes) self.__actionMethod = None def isMinimized(self): """ Public method to check the minimized state. @return flag indicating the minimized state (boolean) """ return self.__minimized def isAutoHiding(self): """ Public method to check, if the auto hide function is active. @return flag indicating the state of auto hiding (boolean) """ return self.__autoHide def eventFilter(self, obj, evt): """ Public method to handle some events for the tabbar. @param obj reference to the object (QObject) @param evt reference to the event object (QEvent) @return flag indicating, if the event was handled (boolean) """ if obj == self.__tabBar: if evt.type() == QEvent.MouseButtonPress: pos = evt.pos() for i in range(self.__tabBar.count()): if self.__tabBar.tabRect(i).contains(pos): break if i == self.__tabBar.currentIndex(): if self.isMinimized(): self.expand() else: self.shrink() return True elif self.isMinimized(): self.expand() elif evt.type() == QEvent.Wheel: if qVersion() >= "5.0.0": delta = evt.angleDelta().y() else: delta = evt.delta() if delta > 0: self.prevTab() else: self.nextTab() return True return QWidget.eventFilter(self, obj, evt) def addTab(self, widget, iconOrLabel, label=None): """ Public method to add a tab to the sidebar. @param widget reference to the widget to add (QWidget) @param iconOrLabel reference to the icon or the label text of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.addTab(iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.addTab(iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.addWidget(widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def insertTab(self, index, widget, iconOrLabel, label=None): """ Public method to insert a tab into the sidebar. @param index the index to insert the tab at (integer) @param widget reference to the widget to insert (QWidget) @param iconOrLabel reference to the icon or the labeltext of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.insertTab(index, iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.insertTab(index, iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.insertWidget(index, widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def removeTab(self, index): """ Public method to remove a tab. @param index the index of the tab to remove (integer) """ self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index)) self.__tabBar.removeTab(index) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def clear(self): """ Public method to remove all tabs. """ while self.count() > 0: self.removeTab(0) def prevTab(self): """ Public slot used to show the previous tab. """ ind = self.currentIndex() - 1 if ind == -1: ind = self.count() - 1 self.setCurrentIndex(ind) self.currentWidget().setFocus() def nextTab(self): """ Public slot used to show the next tab. """ ind = self.currentIndex() + 1 if ind == self.count(): ind = 0 self.setCurrentIndex(ind) self.currentWidget().setFocus() def count(self): """ Public method to get the number of tabs. @return number of tabs in the sidebar (integer) """ return self.__tabBar.count() def currentIndex(self): """ Public method to get the index of the current tab. @return index of the current tab (integer) """ return self.__stackedWidget.currentIndex() def setCurrentIndex(self, index): """ Public slot to set the current index. @param index the index to set as the current index (integer) """ self.__tabBar.setCurrentIndex(index) self.__stackedWidget.setCurrentIndex(index) if self.isMinimized(): self.expand() def currentWidget(self): """ Public method to get a reference to the current widget. @return reference to the current widget (QWidget) """ return self.__stackedWidget.currentWidget() def setCurrentWidget(self, widget): """ Public slot to set the current widget. @param widget reference to the widget to become the current widget (QWidget) """ self.__stackedWidget.setCurrentWidget(widget) self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex()) if self.isMinimized(): self.expand() def indexOf(self, widget): """ Public method to get the index of the given widget. @param widget reference to the widget to get the index of (QWidget) @return index of the given widget (integer) """ return self.__stackedWidget.indexOf(widget) def isTabEnabled(self, index): """ Public method to check, if a tab is enabled. @param index index of the tab to check (integer) @return flag indicating the enabled state (boolean) """ return self.__tabBar.isTabEnabled(index) def setTabEnabled(self, index, enabled): """ Public method to set the enabled state of a tab. @param index index of the tab to set (integer) @param enabled enabled state to set (boolean) """ self.__tabBar.setTabEnabled(index, enabled) def orientation(self): """ Public method to get the orientation of the sidebar. @return orientation of the sidebar (North, East, South, West) """ return self.__orientation def setOrientation(self, orient): """ Public method to set the orientation of the sidebar. @param orient orientation of the sidebar (North, East, South, West) """ if orient == E5SideBar.North: self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.TopToBottom) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.East: self.__tabBar.setShape(QTabBar.RoundedEast) self.__tabBar.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.RightToLeft) self.layout.setAlignment(self.barLayout, Qt.AlignTop) elif orient == E5SideBar.South: self.__tabBar.setShape(QTabBar.RoundedSouth) self.__tabBar.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.BottomToTop) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.West: self.__tabBar.setShape(QTabBar.RoundedWest) self.__tabBar.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.LeftToRight) self.layout.setAlignment(self.barLayout, Qt.AlignTop) self.__orientation = orient def tabIcon(self, index): """ Public method to get the icon of a tab. @param index index of the tab (integer) @return icon of the tab (QIcon) """ return self.__tabBar.tabIcon(index) def setTabIcon(self, index, icon): """ Public method to set the icon of a tab. @param index index of the tab (integer) @param icon icon to be set (QIcon) """ self.__tabBar.setTabIcon(index, icon) def tabText(self, index): """ Public method to get the text of a tab. @param index index of the tab (integer) @return text of the tab (string) """ return self.__tabBar.tabText(index) def setTabText(self, index, text): """ Public method to set the text of a tab. @param index index of the tab (integer) @param text text to set (string) """ self.__tabBar.setTabText(index, text) def tabToolTip(self, index): """ Public method to get the tooltip text of a tab. @param index index of the tab (integer) @return tooltip text of the tab (string) """ return self.__tabBar.tabToolTip(index) def setTabToolTip(self, index, tip): """ Public method to set the tooltip text of a tab. @param index index of the tab (integer) @param tip tooltip text to set (string) """ self.__tabBar.setTabToolTip(index, tip) def tabWhatsThis(self, index): """ Public method to get the WhatsThis text of a tab. @param index index of the tab (integer) @return WhatsThis text of the tab (string) """ return self.__tabBar.tabWhatsThis(index) def setTabWhatsThis(self, index, text): """ Public method to set the WhatsThis text of a tab. @param index index of the tab (integer) @param text WhatsThis text to set (string) """ self.__tabBar.setTabWhatsThis(index, text) def widget(self, index): """ Public method to get a reference to the widget associated with a tab. @param index index of the tab (integer) @return reference to the widget (QWidget) """ return self.__stackedWidget.widget(index) def saveState(self): """ Public method to save the state of the sidebar. @return saved state as a byte array (QByteArray) """ if len(self.splitterSizes) == 0: if self.splitter: self.splitterSizes = self.splitter.sizes() self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() data = QByteArray() stream = QDataStream(data, QIODevice.WriteOnly) stream.setVersion(QDataStream.Qt_4_6) stream.writeUInt16(self.Version) stream.writeBool(self.__minimized) stream << self.__bigSize stream.writeUInt16(self.__minSize) stream.writeUInt16(self.__maxSize) stream.writeUInt16(len(self.splitterSizes)) for size in self.splitterSizes: stream.writeUInt16(size) stream.writeBool(self.__autoHide) return data def restoreState(self, state): """ Public method to restore the state of the sidebar. @param state byte array containing the saved state (QByteArray) @return flag indicating success (boolean) """ if state.isEmpty(): return False if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = self.layout.minimumSize().height() maxSize = self.maximumHeight() else: minSize = self.layout.minimumSize().width() maxSize = self.maximumWidth() data = QByteArray(state) stream = QDataStream(data, QIODevice.ReadOnly) stream.setVersion(QDataStream.Qt_4_6) stream.readUInt16() # version minimized = stream.readBool() if minimized and not self.__minimized: self.shrink() stream >> self.__bigSize self.__minSize = max(stream.readUInt16(), minSize) self.__maxSize = max(stream.readUInt16(), maxSize) count = stream.readUInt16() self.splitterSizes = [] for i in range(count): self.splitterSizes.append(stream.readUInt16()) self.__autoHide = stream.readBool() self.__autoHideButton.setChecked(not self.__autoHide) if not minimized: self.expand() return True ####################################################################### ## methods below implement the autohide functionality ####################################################################### def __autoHideToggled(self, checked): """ Private slot to handle the toggling of the autohide button. @param checked flag indicating the checked state of the button (boolean) """ self.__autoHide = not checked if self.__autoHide: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOn.png")) else: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) def __appFocusChanged(self, old, now): """ Private slot to handle a change of the focus. @param old reference to the widget, that lost focus (QWidget or None) @param now reference to the widget having the focus (QWidget or None) """ self.__hasFocus = self.isAncestorOf(now) if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() elif self.__autoHide and self.__hasFocus and self.isMinimized(): self.expand() def enterEvent(self, event): """ Protected method to handle the mouse entering this widget. @param event reference to the event (QEvent) """ if self.__autoHide and self.isMinimized(): self.expand() else: self.__cancelDelayTimer() def leaveEvent(self, event): """ Protected method to handle the mouse leaving this widget. @param event reference to the event (QEvent) """ if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() else: self.__cancelDelayTimer() def shutdown(self): """ Public method to shut down the object. This method does some preparations so the object can be deleted properly. It disconnects from the focusChanged signal in order to avoid trouble later on. """ e5App().focusChanged[QWidget, QWidget].disconnect( self.__appFocusChanged)
class DataSelectionGui(QWidget): """ Manages all GUI elements in the data selection applet. This class itself is the central widget and also owns/manages the applet drawer widgets. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget(self): return self def appletDrawer(self): return self._drawer def menus(self): return [] def viewerControlWidget(self): return self._viewerControlWidgetStack def setImageIndex(self, imageIndex): if imageIndex is not None: self.laneSummaryTableView.selectRow(imageIndex) for detailWidget in self._detailViewerWidgets: detailWidget.selectRow(imageIndex) def stopAndCleanUp(self): self._cleaning_up = True for fn in self.__cleanup_fns: fn() for editor in list(self.volumeEditors.values()): self.viewerStack.removeWidget(editor) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget()) editor.stopAndCleanUp() self.volumeEditors.clear() def imageLaneAdded(self, laneIndex): if len(self.laneSummaryTableView.selectedIndexes()) == 0: self.laneSummaryTableView.selectRow(laneIndex) # We don't have any real work to do because this gui initiated the lane addition in the first place if self.guiMode != GuiMode.Batch: if len(self.topLevelOperator.DatasetGroup) != laneIndex + 1: import warnings warnings.warn( "DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.DatasetGroup), laneIndex)) def imageLaneRemoved(self, laneIndex, finalLength): # There's nothing to do here because the GUI already # handles operator resizes via slot callbacks. pass def allowLaneSelectionChange(self): return False ########################################### ########################################### class UserCancelledError(Exception): # This exception type is raised when the user cancels the # addition of dataset files in the middle of the process somewhere. # It isn't an error -- it's used for control flow. pass def __init__( self, parentApplet, dataSelectionOperator, serializer, instructionText, guiMode=GuiMode.Normal, max_lanes=None, show_axis_details=False, ): """ Constructor. :param dataSelectionOperator: The top-level operator. Must be of type :py:class:`OpMultiLaneDataSelectionGroup`. :param serializer: The applet's serializer. Must be of type :py:class:`DataSelectionSerializer` :param instructionText: A string to display in the applet drawer. :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``. Currently, there is no difference between normal and batch mode. :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow. If ``None``, there is no maximum. """ super(DataSelectionGui, self).__init__() self._cleaning_up = False self.__cleanup_fns = [] self.parentApplet = parentApplet self._max_lanes = max_lanes self.show_axis_details = show_axis_details self._viewerControls = QWidget() self.topLevelOperator = dataSelectionOperator self.guiMode = guiMode self.serializer = serializer self.threadRouter = ThreadRouter(self) self._initCentralUic() self._initAppletDrawerUic(instructionText) self._viewerControlWidgetStack = QStackedWidget(self) self._default_h5n5_volumes: Dict[int, Set[str]] = {} self.__cleanup_fns.append( self.topLevelOperator.DatasetGroup.notifyRemove( bind(self._handleImageRemove))) opWorkflow = self.topLevelOperator.parent assert hasattr( opWorkflow.shell, "onSaveProjectActionTriggered" ), "This class uses the IlastikShell.onSaveProjectActionTriggered function. Did you rename it?" def get_project_file(self) -> h5py.File: return self.topLevelOperator.ProjectFile.value def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] + "/" uic.loadUi(localDir + "/dataSelection.ui", self) self._initTableViews() self._initViewerStack() self.splitter.setSizes([150, 850]) def _initAppletDrawerUic(self, instructionText): """ Load the ui file for the applet drawer, which we own. """ localDir = os.path.split(__file__)[0] + "/" self._drawer = uic.loadUi(localDir + "/dataSelectionDrawer.ui") self._drawer.instructionLabel.setText(instructionText) @threadRouted def _handleImageRemove(self, multislot, index, finalLength): # Remove the viewer for this dataset datasetSlot = self.topLevelOperator.DatasetGroup[index] if datasetSlot in list(self.volumeEditors.keys()): editor = self.volumeEditors[datasetSlot] self.viewerStack.removeWidget(editor) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget()) editor.stopAndCleanUp() @threadRouted def _update_add_button_status(self, viewer): if self._max_lanes: opTop = self.topLevelOperator status = len(opTop.DatasetGroup) < self._max_lanes viewer.setEnabled(status) @threadRouted def _update_summary_buttons_status(self, *args): if self._max_lanes: opTop = self.topLevelOperator status = len(opTop.DatasetGroup) < self._max_lanes for button in self.laneSummaryTableView.addFilesButtons.values(): try: button.setEnabled(status) except RuntimeError: # FIXME: Button might be deleted due to a bug (https://github.com/ilastik/ilastik/issues/2380) logger.debug( "Summary button seems to be deleted, cannot execute callback" ) def _initTableViews(self): self.fileInfoTabWidget.setTabText(0, "Summary") self.laneSummaryTableView.setModel( DataLaneSummaryTableModel(self, self.topLevelOperator)) self.laneSummaryTableView.dataLaneSelected.connect(self.showDataset) self.laneSummaryTableView.addFilesRequested.connect(self.addFiles) self.laneSummaryTableView.addStackRequested.connect(self.addStack) self.laneSummaryTableView.removeLanesRequested.connect( self.handleRemoveLaneButtonClicked) # Monitor Lane-changes to enable/disable add files buttons in summary table self.__cleanup_fns.append( self.topLevelOperator.DatasetGroup.notifyRemoved( self._update_summary_buttons_status)) self.__cleanup_fns.append( self.topLevelOperator.DatasetGroup.notifyInserted( self._update_summary_buttons_status)) self._retained = [] # Retain menus so they don't get deleted self._detailViewerWidgets = [] for roleIndex, role in enumerate( self.topLevelOperator.DatasetRoles.value): detailViewer = DatasetDetailedInfoTableView(self) detailViewer.setModel( DatasetDetailedInfoTableModel(self, self.topLevelOperator, roleIndex)) self._detailViewerWidgets.append(detailViewer) # Button detailViewer.addFilesRequested.connect( partial(self.addFiles, roleIndex)) detailViewer.addStackRequested.connect( partial(self.addStack, roleIndex)) detailViewer.addPrecomputedVolumeRequested.connect( partial(self.addPrecomputedVolume, roleIndex)) detailViewer.addRemoteVolumeRequested.connect( partial(self.addDvidVolume, roleIndex)) # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab self.__cleanup_fns.append( self.topLevelOperator.DatasetGroup.notifyInserted( bind(self._update_add_button_status, detailViewer))) self.__cleanup_fns.append( self.topLevelOperator.DatasetGroup.notifyRemoved( bind(self._update_add_button_status, detailViewer))) # Context menu detailViewer.replaceWithFileRequested.connect( partial(self.handleReplaceFile, roleIndex)) detailViewer.replaceWithStackRequested.connect( partial(self.addStack, roleIndex)) detailViewer.editRequested.connect( partial(self.editDatasetInfo, roleIndex)) detailViewer.resetRequested.connect( partial(self.handleClearDatasets, roleIndex)) # Drag-and-drop detailViewer.addFilesRequestedDrop.connect( partial(self.addFileNames, roleIndex=roleIndex)) # Selection handling def showFirstSelectedDataset(_roleIndex, lanes): if lanes: self.showDataset(lanes[0], _roleIndex) detailViewer.dataLaneSelected.connect( partial(showFirstSelectedDataset, roleIndex)) self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role) self.fileInfoTabWidget.currentChanged.connect(self.handleSwitchTabs) self.fileInfoTabWidget.setCurrentIndex(0) def handleSwitchTabs(self, tabIndex): if tabIndex < len(self._detailViewerWidgets): roleIndex = tabIndex # If summary tab is moved to the front, change this line. detailViewer = self._detailViewerWidgets[roleIndex] selectedLanes = detailViewer.selectedLanes if selectedLanes: self.showDataset(selectedLanes[0], roleIndex) def _initViewerStack(self): self.volumeEditors = {} self.viewerStack.addWidget(QWidget()) def handleRemoveLaneButtonClicked(self): """ The user clicked the "Remove" button. Remove the currently selected row(s) from both the GUI and the top-level operator. """ # Figure out which lanes to remove selectedIndexes = self.laneSummaryTableView.selectedIndexes() rows = set() for modelIndex in selectedIndexes: rows.add(modelIndex.row()) # Don't remove the last row, which is just buttons. rows.discard(self.laneSummaryTableView.model().rowCount() - 1) # Remove in reverse order so row numbers remain consistent for row in reversed(sorted(rows)): # Remove lanes from the operator. # The table model will notice the changes and update the rows accordingly. finalSize = len(self.topLevelOperator.DatasetGroup) - 1 self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize) @threadRouted def showDataset(self, laneIndex, roleIndex=None): if self._cleaning_up: return if laneIndex == -1: self.viewerStack.setCurrentIndex(0) return assert threading.current_thread().name == "MainThread" if laneIndex >= len(self.topLevelOperator.DatasetGroup): return datasetSlot = self.topLevelOperator.DatasetGroup[laneIndex] # Create if necessary if datasetSlot not in list(self.volumeEditors.keys()): class DatasetViewer(LayerViewerGui): def moveToTop(self, roleIndex): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return datasetRoles = opLaneView.DatasetRoles.value if roleIndex >= len(datasetRoles): return roleName = datasetRoles[roleIndex] try: layerIndex = [l.name for l in self.layerstack].index(roleName) except ValueError: return else: self.layerstack.selectRow(layerIndex) self.layerstack.moveSelectedToTop() def setupLayers(self): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return [] layers = [] datasetRoles = opLaneView.DatasetRoles.value for roleIndex, slot in enumerate(opLaneView.ImageGroup): if slot.ready(): roleName = datasetRoles[roleIndex] layer = self.createStandardLayerFromSlot(slot) layer.name = roleName layers.append(layer) return layers opLaneView = self.topLevelOperator.getLane(laneIndex) layerViewer = DatasetViewer(self.parentApplet, opLaneView, crosshair=False) # Maximize the x-y view by default. layerViewer.volumeEditorWidget.quadview.ensureMaximized(2) self.volumeEditors[datasetSlot] = layerViewer self.viewerStack.addWidget(layerViewer) self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget()) # Show the right one viewer = self.volumeEditors[datasetSlot] displayedRole = self.fileInfoTabWidget.currentIndex() viewer.moveToTop(displayedRole) self.viewerStack.setCurrentWidget(viewer) self._viewerControlWidgetStack.setCurrentWidget( viewer.viewerControlWidget()) def handleReplaceFile(self, roleIndex, startingLaneNum): self.addFiles(roleIndex, startingLaneNum) def addFiles(self, roleIndex, startingLaneNum=None): """ The user clicked the "Add File" button. Ask him to choose a file (or several) and add them to both the GUI table and the top-level operator inputs. """ # Launch the "Open File" dialog paths = ImageFileDialog(self).getSelectedPaths() self.addFileNames(paths, startingLaneNum, roleIndex) def addFileNames(self, paths: List[Path], startingLaneNum: int, roleIndex: int): # If the user didn't cancel for path in paths or []: try: full_path = self._get_dataset_full_path(path, roleIndex=roleIndex) info = self.instantiate_dataset_info(url=str(full_path), role=roleIndex) self.addLanes([info], roleIndex=roleIndex, startingLaneNum=startingLaneNum) except DataSelectionGui.UserCancelledError: pass except Exception as ex: log_exception(logger) QMessageBox.critical(self, "Error loading file", str(ex)) def _findFirstEmptyLane(self, roleIndex): opTop = self.topLevelOperator # Determine the number of files this role already has # Search for the last valid value. firstNewLane = 0 for laneIndex, slot in reversed( list( zip(list(range(len(opTop.DatasetGroup))), opTop.DatasetGroup))): if slot[roleIndex].ready(): firstNewLane = laneIndex + 1 break return firstNewLane def getNumLanes(self) -> int: return len(self.topLevelOperator.DatasetGroup) def getInfoSlots(self, roleIndex: int): return [ self.topLevelOperator.DatasetGroup[laneIndex][roleIndex] for laneIndex in range(self.getNumLanes()) ] def addLanes(self, new_infos: List[DatasetInfo], roleIndex, startingLaneNum=None): """ Add the given filenames to both the GUI table and the top-level operator inputs. If startingLaneNum is None, the filenames will be *appended* to the role's list of files. """ originalNumLanes = self.getNumLanes() startingLaneNum, endingLaneNum = self._determineLaneRange( new_infos, startingLaneNum) if originalNumLanes < endingLaneNum + 1: self.topLevelOperator.DatasetGroup.resize(endingLaneNum + 1) info_slots = self.getInfoSlots( roleIndex)[startingLaneNum:endingLaneNum + 1] try: if not self.applyDatasetInfos(new_infos, info_slots): self.topLevelOperator.DatasetGroup.resize(originalNumLanes) return self._checkDataFormatWarnings(roleIndex, startingLaneNum, endingLaneNum) # Show the first image self.showDataset(startingLaneNum, roleIndex) # if only adding new lanes, notify the workflow if startingLaneNum >= originalNumLanes: workflow = self.parentApplet.topLevelOperator.parent workflow.handleNewLanesAdded() # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() except Exception as e: self.topLevelOperator.DatasetGroup.resize(originalNumLanes) QMessageBox.critical(self, "File selection error", str(e)) def applyDatasetInfos(self, new_infos: List[DatasetInfo], info_slots: List[Slot]): original_infos = [] def revert(): for slot, original_info in zip(info_slots, original_infos): if original_info is not None: slot.setValue(original_info) try: for new_info, info_slot in zip(new_infos, info_slots): original_infos.append( info_slot.value if info_slot.ready() else None) while True: try: info_slot.setValue(new_info) break except DatasetConstraintError as e: QMessageBox.warning(self, "Incompatible dataset", str(e)) info_editor = DatasetInfoEditorWidget( self, [new_info], self.serializer) if info_editor.exec_() == QDialog.Rejected: revert() return False new_info = info_editor.edited_infos[0] return True except Exception as e: revert() raise e finally: self.parentApplet.appletStateUpdateRequested() def _determineLaneRange(self, infos: List[DatasetInfo], startingLaneNum=None): """ Determine which lanes should be configured if the user wants to add the given infos starting at startingLaneNum. If startingLaneNum is None, assume the user wants to APPEND the files to the role's slots. """ if startingLaneNum is None or startingLaneNum == -1: startingLaneNum = len(self.topLevelOperator.DatasetGroup) endingLane = startingLaneNum + len(infos) - 1 else: assert startingLaneNum < len(self.topLevelOperator.DatasetGroup) max_files = len( self.topLevelOperator.DatasetGroup) - startingLaneNum if len(infos) > max_files: raise Exception( f"You selected {len(infos)} files for {max_files} slots. To add new files use " "the 'Add new...' option in the context menu or the button in the last row." ) endingLane = min(startingLaneNum + len(infos) - 1, len(self.topLevelOperator.DatasetGroup)) if self._max_lanes and endingLane >= self._max_lanes: raise Exception( f"You may not add more than {self._max_lanes} file(s) to this workflow." ) return (startingLaneNum, endingLane) def _add_default_inner_path(self, roleIndex: int, inner_path: str): paths = self._default_h5n5_volumes.get(roleIndex, set()) paths.add(inner_path) self._default_h5n5_volumes[roleIndex] = paths def _get_previously_used_inner_paths(self, roleIndex: int) -> Set[str]: previous_paths = self._default_h5n5_volumes.get(roleIndex, set()) return previous_paths.copy() def _get_dataset_full_path(self, filePath: Path, roleIndex: int) -> Path: if not DatasetInfo.fileHasInternalPaths(filePath): return filePath datasetNames = DatasetInfo.getPossibleInternalPathsFor( filePath.absolute()) if len(datasetNames) == 0: raise RuntimeError(f"File {filePath} has no image datasets") if len(datasetNames) == 1: selected_dataset = datasetNames.pop() else: auto_inner_paths = self._get_previously_used_inner_paths( roleIndex).intersection(set(datasetNames)) if len(auto_inner_paths) == 1: selected_dataset = auto_inner_paths.pop() else: # Ask the user which dataset to choose dlg = SubvolumeSelectionDlg(datasetNames, self) if dlg.exec_() != QDialog.Accepted: raise DataSelectionGui.UserCancelledError() selected_index = dlg.combo.currentIndex() selected_dataset = str(datasetNames[selected_index]) self._add_default_inner_path(roleIndex=roleIndex, inner_path=selected_dataset) return filePath / selected_dataset.lstrip("/") def guess_axistags_for(self, role: Union[str, int], info: DatasetInfo) -> Optional[AxisTags]: if self.parentApplet.num_lanes == 0: return info.axistags lane = self.parentApplet.get_lane(-1) previous_info = lane.get_dataset_info(role) if previous_info is None or previous_info.default_tags == previous_info.axistags: return info.axistags if len(previous_info.axistags) != len( info.axistags) or previous_info.shape5d.c != info.shape5d.c: return info.axistags return previous_info.axistags def instantiate_dataset_info(self, url: str, role: Union[str, int], *info_args, **info_kwargs) -> DatasetInfo: info = self.parentApplet.create_dataset_info(url=url, *info_args, **info_kwargs) if info_kwargs.get("axistags") is not None: return info axistags = self.guess_axistags_for(role=role, info=info) if axistags in (info.axistags, None): return info return self.parentApplet.create_dataset_info(url=url, *info_args, **info_kwargs, axistags=axistags) def _checkDataFormatWarnings(self, roleIndex, startingLaneNum, endingLane): warn_needed = False opTop = self.topLevelOperator for lane_index in range(startingLaneNum, endingLane + 1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.inefficient_format: warn_needed = True if warn_needed: QMessageBox.warning( self, "Inefficient Data Format", "Your data cannot be accessed efficiently in its current format. " "Check the console output for details.\n" "(For HDF5 files, be sure to enable chunking on your dataset.)", ) def addStack(self, roleIndex, laneIndex): """ The user clicked the "Import Stack Files" button. """ stackDlg = StackFileSelectionWidget(self) stackDlg.exec_() if stackDlg.result() != QDialog.Accepted or not stackDlg.selectedFiles: return # FIXME: ask first if stack should be internalized to project file # also, check prefer_2d, size/volume and presence of 'z' to determine this url = os.path.pathsep.join(stackDlg.selectedFiles) stack_info = self.instantiate_dataset_info( url=url, role=roleIndex, sequence_axis=stackDlg.sequence_axis) try: # FIXME: do this inside a Request self.parentApplet.busy = True inner_path = stack_info.importAsLocalDataset( project_file=self.get_project_file()) internal_info = ProjectInternalDatasetInfo( inner_path=inner_path, nickname=stack_info.nickname, project_file=self.get_project_file()) finally: self.parentApplet.busy = False self.addLanes([internal_info], roleIndex, laneIndex) def handleClearDatasets(self, roleIndex, selectedRows): for row in selectedRows: self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect() # Remove all operators that no longer have any connected slots laneIndexes = list(range(len(self.topLevelOperator.DatasetGroup))) for laneIndex, multislot in reversed( list(zip(laneIndexes, self.topLevelOperator.DatasetGroup))): any_ready = False for slot in multislot: any_ready |= slot.ready() if not any_ready: self.topLevelOperator.DatasetGroup.removeSlot( laneIndex, len(self.topLevelOperator.DatasetGroup) - 1) # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() def editDatasetInfo(self, roleIndex, laneIndexes): all_info_slots = self.getInfoSlots(roleIndex) selected_info_slots = [all_info_slots[idx] for idx in laneIndexes] infos = [slot.value for slot in selected_info_slots] editorDlg = DatasetInfoEditorWidget(self, infos, self.serializer) if editorDlg.exec_() == QDialog.Accepted: self.applyDatasetInfos(editorDlg.edited_infos, selected_info_slots) def addPrecomputedVolume(self, roleIndex, laneIndex): # add history... history = [] browser = PrecomputedVolumeBrowser(history=history, parent=self) if browser.exec_() == PrecomputedVolumeBrowser.Rejected: return info = self.instantiate_dataset_info(url=browser.selected_url, role=roleIndex) self.addLanes([info], roleIndex=roleIndex, startingLaneNum=laneIndex) def addDvidVolume(self, roleIndex, laneIndex): group = "DataSelection" recent_hosts_key = "Recent DVID Hosts" recent_hosts = preferences.get(group, recent_hosts_key) if not recent_hosts: recent_hosts = ["localhost:8000"] recent_hosts = [ h for h in recent_hosts if h ] # There used to be a bug where empty strings could be saved. Filter those out. recent_nodes_key = "Recent DVID Nodes" recent_nodes = preferences.get(group, recent_nodes_key) or {} from .dvidDataSelectionBrowser import DvidDataSelectionBrowser browser = DvidDataSelectionBrowser(recent_hosts, recent_nodes, parent=self) if browser.exec_() == DvidDataSelectionBrowser.Rejected: return if None in browser.get_selection(): QMessageBox.critical("Couldn't use your selection.") return rois = None hostname, repo_uuid, volume_name, node_uuid, typename = browser.get_selection( ) dvid_url = f"http://{hostname}/api/node/{node_uuid}/{volume_name}" subvolume_roi = browser.get_subvolume_roi() # Relocate host to top of 'recent' list, and limit list to 10 items. try: i = recent_hosts.index(hostname) del recent_hosts[i] except ValueError: pass finally: recent_hosts.insert(0, hostname) recent_hosts = recent_hosts[:10] # Save pref preferences.set(group, recent_hosts_key, recent_hosts) recent_nodes[hostname] = node_uuid preferences.set(group, recent_nodes_key, recent_nodes) self.addLanes( [UrlDatasetInfo(url=dvid_url, subvolume_roi=subvolume_roi)], roleIndex)
class ProjectManager(QWidget): def __init__(self, project_path, pre_data=None): super().__init__() self.project_path = project_path self.project = { 'name': project_path[project_path.rfind('/') + 1:], 'files': [], 'infos': [] } self.class_view = QStackedWidget() self.file_list = QListWidget() self.file_list.doubleClicked.connect(self.file_name_change) self.file_group = QGroupBox(self.project['name']) if pre_data: self.project['files'] = pre_data['files'] for file, info in zip(self.project['files'], pre_data['infos']): self.file_list.addItem(file) self.project['infos'].append( ClassTreeView(self.project_path + '/' + file, pre_data=info)) self.class_view.addWidget(self.project['infos'][-1]) self.init_ui() def init_ui(self): btn_layout = QHBoxLayout() file_add_button = QPushButton('파일 추가') file_add_button.clicked.connect(self.file_add) btn_layout.addWidget(file_add_button) file_del_button = QPushButton('파일 삭제') file_del_button.clicked.connect(self.file_del) btn_layout.addWidget(file_del_button) self.file_list.clicked.connect(self.change_view) file_layout = QVBoxLayout() file_layout.addWidget(self.file_list) file_layout.addLayout(btn_layout) self.file_group.mouseDoubleClickEvent = self.get_project_name self.file_group.setLayout(file_layout) main_layout = QGridLayout() main_layout.addWidget(self.file_group, 0, 0, 0, 2) main_layout.addWidget(self.class_view, 0, 2) self.setLayout(main_layout) self.setFixedSize(800, 500) def change_view(self): self.class_view.setCurrentIndex( self.file_list.selectedIndexes()[0].row()) def get_project_name(self, event): while True: project_name, success = QInputDialog.getText( self, '프로젝트 이름', '프로젝트 이름을 입력해주세요') new_path = self.project_path[:self.project_path. rfind('/')] + '/' + project_name if not os.path.exists(new_path) or not success: break box = QMessageBox() box.setWindowTitle('경고') box.setText('이미 존재하는 폴더입니다.') box.setModal(True) box.exec_() if success and project_name: self.file_group.setTitle(new_path[new_path.rfind('/') + 1:]) self.project['name'] = new_path[new_path.rfind('/') + 1:] os.rename(self.project_path, new_path) self.project_path = new_path def get_file_name(self): while True: file_name, success = QInputDialog.getText(self, '파일 이름', '파일 이름을 입력해주세요') if file_name.lower() not in [ s.lower() for s in self.project['files'] ]: break box = QMessageBox() box.setWindowTitle('경고') box.setText('이미 존재하는 파일입니다.') box.setModal(True) box.exec_() return file_name, success def file_name_change(self): file_name, success = self.get_file_name() if file_name and success: index = self.file_list.selectedIndexes()[0].row() self.project['files'][index] = file_name if os.path.exists(self.project['infos'][index].file_full_path + '.py'): os.remove(self.project['infos'][index].file_full_path + '.py') self.project['infos'][ index].file_full_path = self.project_path + '/' + file_name self.file_list.selectedItems()[0].setText(file_name) def file_add(self): file_name, success = self.get_file_name() if file_name and success: self.project['files'].append(file_name) self.file_list.addItem(self.project['files'][-1]) self.project['infos'].append( ClassTreeView(self.project_path + '/' + self.project['files'][-1])) self.class_view.addWidget(self.project['infos'][-1]) def file_del(self): selected_item = self.file_list.selectedItems() if not selected_item: return selected_item = selected_item[0] confirm, success = QInputDialog.getText( self, '확인', '정말로 삭제하길 원하면 선택한 파일의 이름을 적으세요') if success and confirm and confirm == selected_item.text(): row = self.file_list.selectedIndexes()[0].row() self.class_view.removeWidget(self.project['infos'][row]) self.project['infos'].pop(row) self.file_list.takeItem(self.file_list.row(selected_item)) self.project['files'].pop(row) if os.path.exists('%s/%s.py' % (self.project_path, confirm)): os.remove('%s/%s.py' % (self.project_path, confirm))
class MainView(base, form): def __init__(self, pipeline, parent=None): super(base, self).__init__(parent) self.setupUi(self) self.pipeline = pipeline self.pip_widgets = [] self.default_pips = [] self.draw_ui() self.connect_ui() def register_observers(self): pass def connect_ui(self): """ This function connects the ui using signals from the ui elements and its method counterparts. """ self.input_btn.clicked.connect(self.set_input_url) self.output_btn.clicked.connect(self.set_output_url) self.save_btn.clicked.connect(self.save_pipeline) self.load_favorite_pipelines() self.fav_pips_combo_box.activated.connect(self.select_default_pip) self.run_btn.clicked.connect(self.run) self.delete_btn.clicked.connect(self.trash_pipeline) self.add_btn.clicked.connect(lambda: self.add_pipe_entry_new()) def draw_ui(self): """ This function draws all additional UI elements. If you want the application to display any additional things like a button you can either add it in the QtDesigner or declare it here. """ # *TODO* Create these ones with Qt Designer and put them into select_cat_alg_vbox_layout. I failed self.ComboxCategories = QComboBox() self.stackedWidgetComboxesAlgorithms = QStackedWidget() self.select_cat_alg_vbox_layout.addWidget(self.ComboxCategories) self.select_cat_alg_vbox_layout.addWidget(self.stackedWidgetComboxesAlgorithms) self.ComboxCategories.hide() """ This function is concerned with drawing all non static elements into the GUI. """ """self.set_pip_title("A. Junius2") self.set_preset(["A.Junius", "test", "test", "test"]) self.add_pip_entry("../assets/images/P.png", "Preprocessing - adaptive trehsold watershed") self.add_pip_entry("../assets/images/P.png", "Preprocessing - adaptive trehsold watershed") self.add_pip_entry("../assets/images/P.png", "Preprocessing - adaptive trehsold watershed") self.add_pip_entry("../assets/images/P.png", "Preprocessing - adaptive trehsold watershed") self.add_pip_entry("../assets/images/P.png", "Preprocessing - adaptive trehsold watershed") self.add_pip_entry("../assets/images/P.png", "Preprocessing - adaptive trehsold watershed") self.add_pip_entry("../assets/images/P.png", "Preprocessing - adaptive trehsold watershed") self.add_cat_image("../assets/images/seg_fav.jpeg", "Preprocessing") self.add_cat_image("../assets/images/wing.jpeg", "Preprocessing") self.add_cat_image("../assets/images/wing.jpeg", "Preprocessing") self.add_cat_image("../assets/images/wing.jpeg", "Preprocessing") self.add_cat_image("../assets/images/wing.jpeg", "Preprocessing") self.add_cat_image("../assets/images/wing.jpeg", "Preprocessing") self.add_cat_image("../assets/images/wing.jpeg", "Preprocessing") self.main_image_label.setPixmap(QtGui.QPixmap("wing.jpeg")) category_combo_box = ComboBoxWidget("type") category_combo_box.add_item("Preprocessing", "../assets/images/P.png") category_combo_box.add_item("Segmentation", "../assets/images/S.png") category_combo_box.add_item("Graph Detection", "../assets/images/D.png") category_combo_box.add_item("Graph Filtering", "../assets/images/F.png") alg_combo_box = ComboBoxWidget("algorithm") alg_combo_box.add_item("Otsus") alg_combo_box.add_item("Guo Hall") alg_combo_box.add_item("Adaptive Treshold") slider_1 = SliderWidget("slider1das", 0, 10, 1, 4, True) slider_2 = SliderWidget("slider1", 0, 10, 2, 4, False) slider_3 = SliderWidget("sliderböadsad", 0, 10, 1, 4, True) slider_4 = SliderWidget("sliderböadsad", 0, 10, 1, 4, True) slider_5 = SliderWidget("sliderböadsad", 0, 10, 1, 4, True) checkbox_1 = CheckBoxWidget("checkbox1", True) self.setting_widget_vbox_layout.addWidget(category_combo_box) self.setting_widget_vbox_layout.addWidget(alg_combo_box) self.setting_widget_vbox_layout.addWidget(slider_1) self.setting_widget_vbox_layout.addWidget(slider_2) self.setting_widget_vbox_layout.addWidget(slider_3) self.setting_widget_vbox_layout.addWidget(slider_4) self.setting_widget_vbox_layout.addWidget(slider_5) self.setting_widget_vbox_layout.addWidget(checkbox_1) self.setting_widget_vbox_layout.setAlignment(Qt.AlignTop)""" def set_pip_title(self, title): """ Sets the title of the current selected pipeline in the ui. Args: | *title*: the title of the pipeline | *label_ref*: the reference to the label. """ self.current_pip_label.setText(title) def load_dark_theme(self, application): """ This function is called to load the white theme with all its icons for the buttons and the css file. Args: application: the cureent app instance """ # load buttons pixmap_icon = QtGui.QPixmap("./assets/images/add_white.png") q_icon = QtGui.QIcon(pixmap_icon) self.add_btn.setIcon(q_icon) pixmap_icon = QtGui.QPixmap("./assets/images/trash_white.png") q_icon = QtGui.QIcon(pixmap_icon) self.delete_btn.setIcon(q_icon) pixmap_icon = QtGui.QPixmap("./assets/images/diskette_white.png") q_icon = QtGui.QIcon(pixmap_icon) self.save_btn.setIcon(q_icon) pixmap_icon = QtGui.QPixmap("./assets/images/up-arrow_white.png") q_icon = QtGui.QIcon(pixmap_icon) self.input_btn.setIcon(q_icon) pixmap_icon = QtGui.QPixmap("./assets/images/folder_white.png") q_icon = QtGui.QIcon(pixmap_icon) self.output_btn.setIcon(q_icon) @pyqtSlot(int) def select_default_pip(self, index): """ This is the slot for the Pipeline combobox in the ui Args: index: index of the option currently selected """ # delete current pipeline self.trash_pipeline() # get url and name name, url = self.default_pips[index - 1] # parse the json in the model self.pipeline.load_pipeline_json(url) print("PARSER" + str(self.pipeline.executed_cats[0].active_algorithm)) print("PARSER" + str(self.pipeline.executed_cats[1].active_algorithm)) # set the title self.set_pip_title(name) # Create an entry in the pipeline widget for every step in the pipeline for i in range(0, len(self.pipeline.executed_cats)): self.add_pipe_entry_new(i) self.scroll_down_pip() """for widget in alg_widgets: self.setting_widget_vbox_layout.addWidget(widget)""" def trash_pipeline(self): """ This method clears the complete pipeline while users clicked the trash button. """ # remove all entries in the pipeline list while self.pip_widget_vbox_layout.count(): child = self.pip_widget_vbox_layout.takeAt(0) child.widget().deleteLater() while self.stackedWidget_Settings.currentWidget() is not None: self.stackedWidget_Settings.removeWidget(self.stackedWidget_Settings.currentWidget()) self.settings_collapsable.setTitle("") # remove the pipeline name self.set_pip_title("") # remove all entries int the executed_cats of the model pipeline del self.pipeline.executed_cats[:] # remove all widgets del self.pip_widgets[:] # remove category algorith dropdown self.remove_cat_alg_dropdown() # remove all entries from the pipeline model del self.pipeline.executed_cats[:] @pyqtSlot() def run(self): """ This method runs the the pipeline by calling the process methode in pipeline """ self.pipeline.process() @pyqtSlot() def set_input_url(self): """ This method sets the url for the input image in the pipeline. """ url = QtWidgets.QFileDialog.getOpenFileNames() if url[0]: print(url[0]) print(url[0][0]) self.lineEdit.setText(url[0][0]) self.pipeline.set_input(url[0][0]) @pyqtSlot() def set_output_url(self): """ This method sets the url for the output folder in the pipeline. Args: url: the url to the output folder a user selected in the ui """ url = QtWidgets.QFileDialog.getExistingDirectory() if url: print(url) print(url) self.custom_line_edit.setText(url) self.pipeline.set_output_dir(url) def load_favorite_pipelines(self): """ Scans the directory for default pipelines to display all available items """ self.fav_pips_combo_box.addItem("Please Select") # scan the directory for default pipelines for file in os.listdir("./_default_pipelines"): if file.endswith(".json"): name = file.split(".")[0] url = os.path.abspath("./_default_pipelines" + "/" + file) self.default_pips.append([name, url]) self.fav_pips_combo_box.addItem(name) @pyqtSlot() def save_pipeline(self): """ Saves the pipeline as a json at the users file system. """ url = str(QtWidgets.QFileDialog.getSaveFileName()[0]) split_list = url.split("/") name = split_list[len(split_list) - 1].split(".")[0] del split_list[len(split_list) - 1] url = url.replace(name, "") self.pipeline.save_pipeline_json(name, url) @pyqtSlot(int) def remove_pip_entry(self, pipe_entry_widget, settings_widget, cat=None): """ Removes the pip entry at the given position in the ui Args: pipeline_index (object): settings_widget: position: position at which the pip entry gets removed """ # remove pipeline entry widget from ui self.pip_widget_vbox_layout.removeWidget(pipe_entry_widget) pipe_entry_widget.deleteLater() # remove it settings widgets from ui if settings_widget is not None: if self.stackedWidget_Settings.currentWidget() == settings_widget: self.stackedWidget_Settings.hide() self.remove_cat_alg_dropdown() self.settings_collapsable.setTitle("Settings") self.stackedWidget_Settings.removeWidget(settings_widget) # remove in model if cat is not None: print("Remove entry at pos " + str(self.pipeline.get_index(cat)) + " " + str(cat)) self.pipeline.delete_category(self.pipeline.get_index(cat)) def change_pip_entry_alg(self, position, new_category, new_algorithm, pipe_entry_widget, settings_widget): """ Changes the selected algorithm of the pipeline entry at the position. Afterwards create all widgets for this algorithm instance Args: position: the position of the pipeline entry algorithm: the selected algorithm for this category """ print("Position to be changed:" + str(position)) print("Pipeline length: " + str(len(self.pipeline.executed_cats))) old_cat = self.pipeline.executed_cats[position] old_alg = old_cat.active_algorithm print("Old Cat found in pipeline: " + str(old_cat)) print("Old Alg: found in pipeline:" + str(old_alg)) print("New Category given:" + str(new_category)) print("New Algorithm given:" + str(new_algorithm)) # set in model self.pipeline.change_category(new_category, position) self.pipeline.change_algorithm(new_algorithm, position) new_cat = self.pipeline.executed_cats[position] new_alg = new_cat.active_algorithm # change settings widgets self.remove_pip_entry(pipe_entry_widget, settings_widget) (new_pipe_entry_widget, new_settings_widget) = self.add_pipe_entry_new(position) self.stackedWidget_Settings.show() self.stackedWidget_Settings.setCurrentIndex(position) self.settings_collapsable.setTitle(new_alg.get_name() + " Settings") self.remove_cat_alg_dropdown() self.create_cat_alg_dropdown(position, new_pipe_entry_widget, new_settings_widget) self.set_cat_alg_dropdown(new_cat, new_alg) print("New Cat found in pipeline: " + str(new_cat)) print("New Alg found in pipeline: " + str(new_alg)) def load_settings_widgets_from_pipeline_groupbox(self, position): """ Extracts all widgets from a single algorithm and returns a QBoxLayout Args: alg: the alg instance we extract from Returns: a QBoxLayout containing all widgets for this particular alg. """ alg = self.pipeline.executed_cats[position].active_algorithm print("alg " + str(alg)) print("cat " + str(self.pipeline.executed_cats[position])) empty_flag = True groupOfSliders = QGroupBox() sp = QSizePolicy() sp.setVerticalPolicy(QSizePolicy.Preferred) # groupOfSliders.setSizePolicy(sp) groupOfSliderssLayout = QBoxLayout(QBoxLayout.TopToBottom) groupOfSliderssLayout.setContentsMargins(0, -0, -0, 0) groupOfSliderssLayout.setAlignment(Qt.AlignTop) groupOfSliderssLayout.setSpacing(0) print("Build Slider @ "+ str(position)) # create integer sliders for slider in alg.integer_sliders: empty_flag = False print("slider.value " + str(slider.value)) print("slider " + str(slider)) #print(alg.get_name() + ": add slider (int).") groupOfSliderssLayout.addWidget( SliderWidget(slider.name, slider.lower, slider.upper, slider.step_size, slider.value, slider.set_value, False)) # create float sliders for slider in alg.float_sliders: empty_flag = False #print(alg.get_name() + ": add slider (float).") groupOfSliderssLayout.addWidget( SliderWidget(slider.name, slider.lower, slider.upper, slider.step_size, slider.value, slider.set_value, True), 0, Qt.AlignTop) # create checkboxes for checkbox in alg.checkboxes: empty_flag = False #print(alg.get_name() + ": add checkbox.") groupOfSliderssLayout.addWidget(CheckBoxWidget(checkbox.name, checkbox.value, checkbox.set_value), 0, Qt.AlignTop) # create dropdowns for combobox in alg.drop_downs: empty_flag = False #print(alg.get_name() + ": add combobox.") groupOfSliderssLayout.addWidget( ComboBoxWidget(combobox.name, combobox.options, combobox.set_value, combobox.value), 0, Qt.AlignTop) if empty_flag: label = QLabel() label.setText("This algorithm has no Settings.") groupOfSliderssLayout.addWidget(label, 0, Qt.AlignHCenter) groupOfSliders.setLayout(groupOfSliderssLayout) return groupOfSliders def create_cat_alg_dropdown(self, cat_position, pipe_entry_widget, settings_widget): """ Args: last_cat (object): """ layout = self.select_cat_alg_vbox_layout cat = self.pipeline.executed_cats[cat_position] last_cat = None # Show only allowed categories in dropdown if len(self.pipeline.executed_cats) > 1: last_cat = self.pipeline.executed_cats[cat_position - 1] # Combobox for selecting Category self.ComboxCategories.show() self.ComboxCategories.setFixedHeight(30) self.ComboxCategories.addItem("<Please Select Category>") self.stackedWidgetComboxesAlgorithms = QStackedWidget() self.stackedWidgetComboxesAlgorithms.setFixedHeight(30) self.stackedWidgetComboxesAlgorithms.hide() def setCurrentIndexCat(index): #print("Set Cat") if self.ComboxCategories.currentIndex() == 0: self.stackedWidgetComboxesAlgorithms.hide() else: self.stackedWidgetComboxesAlgorithms.show() self.stackedWidgetComboxesAlgorithms.setCurrentIndex(index - 1) for category_name in self.pipeline.report_available_cats(last_cat): # Add Category to combobox self.ComboxCategories.addItem(category_name) tmp1 = QComboBox() tmp1.addItem("<Please Select Algorithm>") tmp1.setFixedHeight(30) category = self.pipeline.get_category(category_name) #self.current_index = -1 def setCurrentIndexAlg(index): if self.ComboxCategories.currentIndex() == 0 or self.stackedWidgetComboxesAlgorithms.currentWidget().currentIndex() == 0: pass else: #self.current_index != index: self.change_pip_entry_alg(self.pipeline.get_index(cat), self.ComboxCategories.currentText(), self.stackedWidgetComboxesAlgorithms.currentWidget().currentText(), pipe_entry_widget, settings_widget) #self.current_index = index tmp1.activated.connect(setCurrentIndexAlg) for algorithm_name in self.pipeline.get_all_algorithm_list(category): tmp1.addItem(algorithm_name) self.stackedWidgetComboxesAlgorithms.addWidget(tmp1) layout.addWidget(self.ComboxCategories) layout.addWidget(self.stackedWidgetComboxesAlgorithms) self.ComboxCategories.activated.connect(setCurrentIndexCat) def set_cat_alg_dropdown(self, category, algorithm): indexC = self.ComboxCategories.findText(category.get_name()) #print("IndexC " + str(indexC)) self.ComboxCategories.setCurrentIndex(indexC) self.stackedWidgetComboxesAlgorithms.show() self.stackedWidgetComboxesAlgorithms.setCurrentIndex(indexC - 1) indexA = self.stackedWidgetComboxesAlgorithms.currentWidget().findText(algorithm.get_name()) #print("IndexA " + str(indexA)) self.stackedWidgetComboxesAlgorithms.currentWidget().setCurrentIndex(indexA) def remove_cat_alg_dropdown(self): """ Returns: object: """ self.ComboxCategories.clear() while self.stackedWidgetComboxesAlgorithms.currentWidget() is not None: self.stackedWidgetComboxesAlgorithms.removeWidget(self.stackedWidgetComboxesAlgorithms.currentWidget()) while self.select_cat_alg_vbox_layout.count(): child = self.select_cat_alg_vbox_layout.takeAt(0) child.widget().hide() def scroll_down_pip(self): self.pip_scroll.verticalScrollBar().setSliderPosition(self.pip_scroll.verticalScrollBar().maximum()) def add_pipe_entry_new(self, position=None): """ Creates a entry in the ui pipeline with a given position in pipeline. It also creates the corresponding settings widget. """ # create an widget that displays the pip entry in the ui and connect the remove button pip_main_widget = QWidget() pip_main_widget.setFixedHeight(70) pip_main_widget.setFixedWidth(300) pip_main_layout = QHBoxLayout() pip_main_widget.setLayout(pip_main_layout) new_marker = False if position is None: position = len(self.pipeline.executed_cats) cat = self.pipeline.new_category(position) label = "<Click to specify new step>" icon = None new_marker = True else: cat = self.pipeline.executed_cats[position] alg = cat.active_algorithm label = alg.get_name() icon = cat.get_icon() new_marker = False pixmap = QPixmap(icon) pixmap_scaled_keeping_aspec = pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio) pixmap_label = QtWidgets.QLabel() pixmap_label.setPixmap(pixmap_scaled_keeping_aspec) pip_up_down = QWidget() pip_up_down.setFixedHeight(70) pip_up_down_layout = QVBoxLayout() pip_up_down.setLayout(pip_up_down_layout) up_btn = QToolButton() dw_btn = QToolButton() up_btn.setArrowType(Qt.UpArrow) up_btn.setFixedHeight(25) dw_btn.setArrowType(Qt.DownArrow) dw_btn.setFixedHeight(25) pip_up_down_layout.addWidget(up_btn) pip_up_down_layout.addWidget(dw_btn) string_label = QLabel() string_label.setText(label) string_label.setFixedWidth(210) btn = QtWidgets.QPushButton() btn.setFixedSize(20, 20) pixmap_icon = QtGui.QPixmap("./assets/images/delete_x_white.png") q_icon = QtGui.QIcon(pixmap_icon) btn.setIcon(q_icon) pip_main_layout.addWidget(pip_up_down, Qt.AlignVCenter) pip_main_layout.addWidget(pixmap_label, Qt.AlignVCenter) pip_main_layout.addWidget(string_label, Qt.AlignLeft) pip_main_layout.addWidget(btn, Qt.AlignVCenter) self.pip_widget_vbox_layout.insertWidget(position, pip_main_widget) # Create the corresponding settings widget and connect it self.settings_collapsable.setTitle("Settings") self.stackedWidget_Settings.hide() settings_main_widget = None if not new_marker: settings_main_widget = self.load_settings_widgets_from_pipeline_groupbox(position) self.stackedWidget_Settings.insertWidget(position, settings_main_widget) def show_settings(): # Set background color while widget is selected. Doesn't work because of theme? *TODO* p = pip_main_widget.palette() p.setColor(pip_main_widget.backgroundRole(), Qt.red) pip_main_widget.setPalette(p) if not new_marker: self.stackedWidget_Settings.show() self.stackedWidget_Settings.setCurrentIndex(self.pipeline.get_index(cat)) self.settings_collapsable.setTitle(alg.get_name() + " Settings") else: self.stackedWidget_Settings.hide() # Create drop down for cats and algs self.remove_cat_alg_dropdown() self.create_cat_alg_dropdown(self.pipeline.get_index(cat), pip_main_widget, settings_main_widget) if not new_marker: self.set_cat_alg_dropdown(cat, alg) # Connect Button to remove step from pipeline def delete_button_clicked(): self.remove_cat_alg_dropdown() self.remove_pip_entry(pip_main_widget, settings_main_widget, cat) self.clickable(pixmap_label).connect(show_settings) self.clickable(string_label).connect(show_settings) btn.clicked.connect(delete_button_clicked) return (pip_main_widget, settings_main_widget) def add_pip_entry_empty(self): """ Creates an blank entry in the ui pipeline since the user still needs to specify a type and an algorithm of the category. It also creates the corresponding settings widget. """ # create an widget that displays the pip entry in the ui and connect the remove button pip_main_widget = QWidget() pip_main_widget.setFixedHeight(70) pip_main_widget.setFixedWidth(300) pip_main_layout = QHBoxLayout() pip_main_widget.setLayout(pip_main_layout) label = "<Click to specify new step>" icon = None pixmap = QPixmap(icon) pixmap_scaled_keeping_aspec = pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio) pixmap_label = QtWidgets.QLabel() pixmap_label.setPixmap(pixmap_scaled_keeping_aspec) pip_up_down = QWidget() pip_up_down.setFixedHeight(70) pip_up_down_layout = QVBoxLayout() pip_up_down.setLayout(pip_up_down_layout) up_btn = QToolButton() dw_btn = QToolButton() up_btn.setArrowType(Qt.UpArrow) up_btn.setFixedHeight(25) dw_btn.setArrowType(Qt.DownArrow) dw_btn.setFixedHeight(25) pip_up_down_layout.addWidget(up_btn) pip_up_down_layout.addWidget(dw_btn) string_label = QLabel() string_label.setText(label) string_label.setFixedWidth(210) btn = QtWidgets.QPushButton() btn.setFixedSize(20, 20) pixmap_icon = QtGui.QPixmap("./assets/images/delete_x_white.png") q_icon = QtGui.QIcon(pixmap_icon) btn.setIcon(q_icon) pip_main_layout.addWidget(pip_up_down, Qt.AlignVCenter) pip_main_layout.addWidget(pixmap_label, Qt.AlignVCenter) pip_main_layout.addWidget(string_label, Qt.AlignLeft) pip_main_layout.addWidget(btn, Qt.AlignVCenter) cat_position = len(self.pipeline.executed_cats) self.pip_widget_vbox_layout.insertWidget(cat_position, pip_main_widget) index = self.pip_widget_vbox_layout.indexOf(pip_main_widget) #print(index) # Create the corresponding empty settings widget and connect it # settings = self.load_widgets_from_cat_groupbox(cat_position) *TODO* EMPTY self.settings_collapsable.setTitle("Settings") self.stackedWidget_Settings.hide() # Add new step to pipeline new_category = self.pipeline.new_category(cat_position) print("Create new entry " + str(new_category)) print("Pipeline length: " + str(len(self.pipeline.executed_cats)) + ".") settings_main_widget = None # Connect pipeline entry with corresponding settings widget def show_settings(): #print("click") self.stackedWidget_Settings.show() self.remove_cat_alg_dropdown() # Create drop down for cats and algs self.create_cat_alg_dropdown(self.pipeline.get_index(new_category), pip_main_widget, settings_main_widget) self.stackedWidget_Settings.hide() # Connect Button to remove step from pipeline def delete_button_clicked(): self.remove_cat_alg_dropdown() self.remove_pip_entry(pip_main_widget, settings_main_widget, new_category) self.clickable(pixmap_label).connect(show_settings) self.clickable(string_label).connect(show_settings) btn.clicked.connect(delete_button_clicked) self.scroll_down_pip() def add_pip_entry(self, cat_position): """ Creates a entry in the ui pipeline with a given position in pipeline. It also creates the corresponding settings widget. """ # create an widget that displays the pip entry in the ui and connect the remove button pip_main_widget = QWidget() pip_main_widget.setFixedHeight(70) pip_main_widget.setFixedWidth(300) pip_main_layout = QHBoxLayout() pip_main_widget.setLayout(pip_main_layout) cat = self.pipeline.executed_cats[cat_position] alg = cat.active_algorithm label = alg.get_name() icon = cat.get_icon() pixmap = QPixmap(icon) pixmap_scaled_keeping_aspec = pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio) pixmap_label = QtWidgets.QLabel() pixmap_label.setPixmap(pixmap_scaled_keeping_aspec) pip_up_down = QWidget() pip_up_down.setFixedHeight(70) pip_up_down_layout = QVBoxLayout() pip_up_down.setLayout(pip_up_down_layout) up_btn = QToolButton() dw_btn = QToolButton() up_btn.setArrowType(Qt.UpArrow) up_btn.setFixedHeight(25) dw_btn.setArrowType(Qt.DownArrow) dw_btn.setFixedHeight(25) pip_up_down_layout.addWidget(up_btn) pip_up_down_layout.addWidget(dw_btn) string_label = QLabel() string_label.setText(label) string_label.setFixedWidth(210) btn = QtWidgets.QPushButton() btn.setFixedSize(20, 20) pixmap_icon = QtGui.QPixmap("./assets/images/delete_x_white.png") q_icon = QtGui.QIcon(pixmap_icon) btn.setIcon(q_icon) pip_main_layout.addWidget(pip_up_down, Qt.AlignVCenter) pip_main_layout.addWidget(pixmap_label, Qt.AlignVCenter) pip_main_layout.addWidget(string_label, Qt.AlignLeft) pip_main_layout.addWidget(btn, Qt.AlignVCenter) self.pip_widget_vbox_layout.insertWidget(cat_position, pip_main_widget) index = self.pip_widget_vbox_layout.indexOf(pip_main_widget) #print(index) # Create the corresponding settings widget and connect it settings_main_widget = self.load_settings_widgets_from_pipeline_groupbox(cat_position) self.settings_collapsable.setTitle("Settings") self.stackedWidget_Settings.hide() self.stackedWidget_Settings.insertWidget(cat_position, settings_main_widget) #print("Read from pipeline entry " + str(cat)) #print("Pipeline length: " + str(len(self.pipeline.executed_cats)) + ".") def show_settings(): # Set background color while widget is selected. Doesn't work because of theme? *TODO* p = pip_main_widget.palette() p.setColor(pip_main_widget.backgroundRole(), Qt.red) pip_main_widget.setPalette(p) self.stackedWidget_Settings.show() self.stackedWidget_Settings.setCurrentIndex(self.pipeline.get_index(cat)) self.settings_collapsable.setTitle(alg.get_name() + " Settings") self.remove_cat_alg_dropdown() # Create drop down for cats and algs self.create_cat_alg_dropdown(self.pipeline.get_index(cat), pip_main_widget, settings_main_widget) #print(cat) #print(alg) self.set_cat_alg_dropdown(cat, alg) # Connect Button to remove step from pipeline def delete_button_clicked(): self.remove_pip_entry(pip_main_widget, settings_main_widget, cat) self.clickable(pixmap_label).connect(show_settings) self.clickable(string_label).connect(show_settings) btn.clicked.connect(delete_button_clicked) return (pip_main_widget, settings_main_widget) # https://wiki.python.org/moin/PyQt/Making%20non-clickable%20widgets%20clickable def clickable(self, widget): """ Convert any widget to a clickable widget. """ class Filter(QObject): clicked = pyqtSignal() def eventFilter(self, obj, event): if obj == widget: if event.type() == QEvent.MouseButtonPress: if obj.rect().contains(event.pos()): self.clicked.emit() # The developer can opt for .emit(obj) to get the object within the slot. return True return False filter = Filter(widget) widget.installEventFilter(filter) return filter.clicked
class ConfigurationWidget(QWidget): """ Class implementing a dialog for the configuration of eric6. @signal preferencesChanged() emitted after settings have been changed @signal masterPasswordChanged(str, str) emitted after the master password has been changed with the old and the new password @signal accepted() emitted to indicate acceptance of the changes @signal rejected() emitted to indicate rejection of the changes """ preferencesChanged = pyqtSignal() masterPasswordChanged = pyqtSignal(str, str) accepted = pyqtSignal() rejected = pyqtSignal() DefaultMode = 0 HelpBrowserMode = 1 TrayStarterMode = 2 HexEditorMode = 3 def __init__(self, parent=None, fromEric=True, displayMode=DefaultMode, expandedEntries=[]): """ Constructor @param parent The parent widget of this dialog. (QWidget) @keyparam fromEric flag indicating a dialog generation from within the eric6 ide (boolean) @keyparam displayMode mode of the configuration dialog (DefaultMode, HelpBrowserMode, TrayStarterMode, HexEditorMode) @exception RuntimeError raised to indicate an invalid dialog mode @keyparam expandedEntries list of entries to be shown expanded (list of strings) """ assert displayMode in ( ConfigurationWidget.DefaultMode, ConfigurationWidget.HelpBrowserMode, ConfigurationWidget.TrayStarterMode, ConfigurationWidget.HexEditorMode, ) super(ConfigurationWidget, self).__init__(parent) self.fromEric = fromEric self.displayMode = displayMode self.__setupUi() self.itmDict = {} if not fromEric: from PluginManager.PluginManager import PluginManager try: self.pluginManager = e5App().getObject("PluginManager") except KeyError: self.pluginManager = PluginManager(self) e5App().registerObject("PluginManager", self.pluginManager) if displayMode == ConfigurationWidget.DefaultMode: self.configItems = { # key : [display string, pixmap name, dialog module name or # page creation function, parent key, # reference to configuration page (must always be last)] # The dialog module must have the module function 'create' to # create the configuration page. This must have the method # 'save' to save the settings. "applicationPage": [self.tr("Application"), "preferences-application.png", "ApplicationPage", None, None], "cooperationPage": [self.tr("Cooperation"), "preferences-cooperation.png", "CooperationPage", None, None], "corbaPage": [self.tr("CORBA"), "preferences-orbit.png", "CorbaPage", None, None], "emailPage": [self.tr("Email"), "preferences-mail_generic.png", "EmailPage", None, None], "graphicsPage": [self.tr("Graphics"), "preferences-graphics.png", "GraphicsPage", None, None], "hexEditorPage": [self.tr("Hex Editor"), "hexEditor.png", "HexEditorPage", None, None], "iconsPage": [self.tr("Icons"), "preferences-icons.png", "IconsPage", None, None], "ircPage": [self.tr("IRC"), "irc.png", "IrcPage", None, None], "logViewerPage": [self.tr("Log-Viewer"), "preferences-logviewer.png", "LogViewerPage", None, None], "mimeTypesPage": [self.tr("Mimetypes"), "preferences-mimetypes.png", "MimeTypesPage", None, None], "networkPage": [self.tr("Network"), "preferences-network.png", "NetworkPage", None, None], "notificationsPage": [self.tr("Notifications"), "preferences-notifications.png", "NotificationsPage", None, None], "pluginManagerPage": [self.tr("Plugin Manager"), "preferences-pluginmanager.png", "PluginManagerPage", None, None], "printerPage": [self.tr("Printer"), "preferences-printer.png", "PrinterPage", None, None], "pythonPage": [self.tr("Python"), "preferences-python.png", "PythonPage", None, None], "qtPage": [self.tr("Qt"), "preferences-qtlogo.png", "QtPage", None, None], "securityPage": [self.tr("Security"), "preferences-security.png", "SecurityPage", None, None], "shellPage": [self.tr("Shell"), "preferences-shell.png", "ShellPage", None, None], "tasksPage": [self.tr("Tasks"), "task.png", "TasksPage", None, None], "templatesPage": [self.tr("Templates"), "preferences-template.png", "TemplatesPage", None, None], "trayStarterPage": [self.tr("Tray Starter"), "erict.png", "TrayStarterPage", None, None], "vcsPage": [self.tr("Version Control Systems"), "preferences-vcs.png", "VcsPage", None, None], "0debuggerPage": [self.tr("Debugger"), "preferences-debugger.png", None, None, None], "debuggerGeneralPage": [self.tr("General"), "preferences-debugger.png", "DebuggerGeneralPage", "0debuggerPage", None], "debuggerPythonPage": [self.tr("Python"), "preferences-pyDebugger.png", "DebuggerPythonPage", "0debuggerPage", None], "debuggerPython3Page": [self.tr("Python3"), "preferences-pyDebugger.png", "DebuggerPython3Page", "0debuggerPage", None], "0editorPage": [self.tr("Editor"), "preferences-editor.png", None, None, None], "editorAPIsPage": [self.tr("APIs"), "preferences-api.png", "EditorAPIsPage", "0editorPage", None], "editorAutocompletionPage": [self.tr("Autocompletion"), "preferences-autocompletion.png", "EditorAutocompletionPage", "0editorPage", None], "editorAutocompletionQScintillaPage": [self.tr("QScintilla"), "qscintilla.png", "EditorAutocompletionQScintillaPage", "editorAutocompletionPage", None], "editorCalltipsPage": [self.tr("Calltips"), "preferences-calltips.png", "EditorCalltipsPage", "0editorPage", None], "editorCalltipsQScintillaPage": [self.tr("QScintilla"), "qscintilla.png", "EditorCalltipsQScintillaPage", "editorCalltipsPage", None], "editorGeneralPage": [self.tr("General"), "preferences-general.png", "EditorGeneralPage", "0editorPage", None], "editorFilePage": [self.tr("Filehandling"), "preferences-filehandling.png", "EditorFilePage", "0editorPage", None], "editorSearchPage": [self.tr("Searching"), "preferences-search.png", "EditorSearchPage", "0editorPage", None], "editorSpellCheckingPage": [self.tr("Spell checking"), "preferences-spellchecking.png", "EditorSpellCheckingPage", "0editorPage", None], "editorStylesPage": [self.tr("Style"), "preferences-styles.png", "EditorStylesPage", "0editorPage", None], "editorSyntaxPage": [self.tr("Code Checkers"), "preferences-debugger.png", "EditorSyntaxPage", "0editorPage", None], "editorTypingPage": [self.tr("Typing"), "preferences-typing.png", "EditorTypingPage", "0editorPage", None], "editorExportersPage": [self.tr("Exporters"), "preferences-exporters.png", "EditorExportersPage", "0editorPage", None], "1editorLexerPage": [self.tr("Highlighters"), "preferences-highlighting-styles.png", None, "0editorPage", None], "editorHighlightersPage": [self.tr("Filetype Associations"), "preferences-highlighter-association.png", "EditorHighlightersPage", "1editorLexerPage", None], "editorHighlightingStylesPage": [self.tr("Styles"), "preferences-highlighting-styles.png", "EditorHighlightingStylesPage", "1editorLexerPage", None], "editorKeywordsPage": [self.tr("Keywords"), "preferences-keywords.png", "EditorKeywordsPage", "1editorLexerPage", None], "editorPropertiesPage": [self.tr("Properties"), "preferences-properties.png", "EditorPropertiesPage", "1editorLexerPage", None], "1editorMouseClickHandlers": [self.tr("Mouse Click Handlers"), "preferences-mouse-click-handler.png", "EditorMouseClickHandlerPage", "0editorPage", None], "0helpPage": [self.tr("Help"), "preferences-help.png", None, None, None], "helpDocumentationPage": [self.tr("Help Documentation"), "preferences-helpdocumentation.png", "HelpDocumentationPage", "0helpPage", None], "helpViewersPage": [self.tr("Help Viewers"), "preferences-helpviewers.png", "HelpViewersPage", "0helpPage", None], "0projectPage": [self.tr("Project"), "preferences-project.png", None, None, None], "projectBrowserPage": [self.tr("Project Viewer"), "preferences-project.png", "ProjectBrowserPage", "0projectPage", None], "projectPage": [self.tr("Project"), "preferences-project.png", "ProjectPage", "0projectPage", None], "multiProjectPage": [self.tr("Multiproject"), "preferences-multiproject.png", "MultiProjectPage", "0projectPage", None], "0interfacePage": [self.tr("Interface"), "preferences-interface.png", None, None, None], "interfacePage": [self.tr("Interface"), "preferences-interface.png", "InterfacePage", "0interfacePage", None], "viewmanagerPage": [self.tr("Viewmanager"), "preferences-viewmanager.png", "ViewmanagerPage", "0interfacePage", None], } try: from PyQt5 import QtWebKit # __IGNORE_WARNING__ self.configItems.update({ "helpAppearancePage": [self.tr("Appearance"), "preferences-styles.png", "HelpAppearancePage", "0helpPage", None], "helpFlashCookieManagerPage": [self.tr("Flash Cookie Manager"), "flashCookie16.png", "HelpFlashCookieManagerPage", "0helpPage", None], "helpVirusTotalPage": [self.tr("VirusTotal Interface"), "virustotal.png", "HelpVirusTotalPage", "0helpPage", None], "helpWebBrowserPage": [self.tr("eric6 Web Browser"), "ericWeb.png", "HelpWebBrowserPage", "0helpPage", None], }) except ImportError: pass self.configItems.update( e5App().getObject("PluginManager").getPluginConfigData()) elif displayMode == ConfigurationWidget.HelpBrowserMode: self.configItems = { # key : [display string, pixmap name, dialog module name or # page creation function, parent key, # reference to configuration page (must always be last)] # The dialog module must have the module function 'create' to # create the configuration page. This must have the method # 'save' to save the settings. "interfacePage": [self.tr("Interface"), "preferences-interface.png", "HelpInterfacePage", None, None], "networkPage": [self.tr("Network"), "preferences-network.png", "NetworkPage", None, None], "printerPage": [self.tr("Printer"), "preferences-printer.png", "PrinterPage", None, None], "securityPage": [self.tr("Security"), "preferences-security.png", "SecurityPage", None, None], "0helpPage": [self.tr("Help"), "preferences-help.png", None, None, None], "helpDocumentationPage": [self.tr("Help Documentation"), "preferences-helpdocumentation.png", "HelpDocumentationPage", "0helpPage", None], } try: from PyQt5 import QtWebKit # __IGNORE_WARNING__ self.configItems.update({ "helpAppearancePage": [self.tr("Appearance"), "preferences-styles.png", "HelpAppearancePage", "0helpPage", None], "helpFlashCookieManagerPage": [self.tr("Flash Cookie Manager"), "flashCookie16.png", "HelpFlashCookieManagerPage", "0helpPage", None], "helpVirusTotalPage": [self.tr("VirusTotal Interface"), "virustotal.png", "HelpVirusTotalPage", "0helpPage", None], "helpWebBrowserPage": [self.tr("eric6 Web Browser"), "ericWeb.png", "HelpWebBrowserPage", "0helpPage", None], }) except ImportError: pass elif displayMode == ConfigurationWidget.TrayStarterMode: self.configItems = { # key : [display string, pixmap name, dialog module name or # page creation function, parent key, # reference to configuration page (must always be last)] # The dialog module must have the module function 'create' to # create the configuration page. This must have the method # 'save' to save the settings. "trayStarterPage": [self.tr("Tray Starter"), "erict.png", "TrayStarterPage", None, None], } elif displayMode == ConfigurationWidget.HexEditorMode: self.configItems = { # key : [display string, pixmap name, dialog module name or # page creation function, parent key, # reference to configuration page (must always be last)] # The dialog module must have the module function 'create' to # create the configuration page. This must have the method # 'save' to save the settings. "hexEditorPage": [self.tr("Hex Editor"), "hexEditor.png", "HexEditorPage", None, None], } else: raise RuntimeError("Illegal mode value: {0}".format(displayMode)) # generate the list entries self.__expandedEntries = [] for key in sorted(self.configItems.keys()): pageData = self.configItems[key] if pageData[3]: if pageData[3] in self.itmDict: pitm = self.itmDict[pageData[3]] # get the parent item else: continue else: pitm = self.configList self.itmDict[key] = ConfigurationPageItem(pitm, pageData[0], key, pageData[1]) self.itmDict[key].setData(0, Qt.UserRole, key) if (not self.fromEric or displayMode != ConfigurationWidget.DefaultMode or key in expandedEntries): self.itmDict[key].setExpanded(True) self.configList.sortByColumn(0, Qt.AscendingOrder) # set the initial size of the splitter self.configSplitter.setSizes([200, 600]) self.configList.itemActivated.connect(self.__showConfigurationPage) self.configList.itemClicked.connect(self.__showConfigurationPage) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.rejected) if displayMode in [ConfigurationWidget.HelpBrowserMode, ConfigurationWidget.TrayStarterMode, ConfigurationWidget.HexEditorMode]: self.configListSearch.hide() if displayMode not in [ConfigurationWidget.TrayStarterMode, ConfigurationWidget.HexEditorMode]: self.__initLexers() def accept(self): """ Public slot to accept the buttonBox accept signal. """ if not isMacPlatform(): wdg = self.focusWidget() if wdg == self.configList: return self.accepted.emit() def __setupUi(self): """ Private method to perform the general setup of the configuration widget. """ self.setObjectName("ConfigurationDialog") self.resize(900, 650) self.verticalLayout_2 = QVBoxLayout(self) self.verticalLayout_2.setSpacing(6) self.verticalLayout_2.setContentsMargins(6, 6, 6, 6) self.verticalLayout_2.setObjectName("verticalLayout_2") self.configSplitter = QSplitter(self) self.configSplitter.setOrientation(Qt.Horizontal) self.configSplitter.setObjectName("configSplitter") self.configListWidget = QWidget(self.configSplitter) self.leftVBoxLayout = QVBoxLayout(self.configListWidget) self.leftVBoxLayout.setContentsMargins(0, 0, 0, 0) self.leftVBoxLayout.setSpacing(0) self.leftVBoxLayout.setObjectName("leftVBoxLayout") self.configListSearch = E5ClearableLineEdit( self, self.tr("Enter search text...")) self.configListSearch.setObjectName("configListSearch") self.leftVBoxLayout.addWidget(self.configListSearch) self.configList = QTreeWidget() self.configList.setObjectName("configList") self.leftVBoxLayout.addWidget(self.configList) self.configListSearch.textChanged.connect(self.__searchTextChanged) self.scrollArea = QScrollArea(self.configSplitter) self.scrollArea.setFrameShape(QFrame.NoFrame) self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scrollArea.setWidgetResizable(False) self.scrollArea.setObjectName("scrollArea") self.configStack = QStackedWidget() self.configStack.setFrameShape(QFrame.Box) self.configStack.setFrameShadow(QFrame.Sunken) self.configStack.setObjectName("configStack") self.scrollArea.setWidget(self.configStack) self.emptyPage = QWidget() self.emptyPage.setGeometry(QRect(0, 0, 372, 591)) self.emptyPage.setObjectName("emptyPage") self.vboxlayout = QVBoxLayout(self.emptyPage) self.vboxlayout.setSpacing(6) self.vboxlayout.setContentsMargins(6, 6, 6, 6) self.vboxlayout.setObjectName("vboxlayout") spacerItem = QSpacerItem( 20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding) self.vboxlayout.addItem(spacerItem) self.emptyPagePixmap = QLabel(self.emptyPage) self.emptyPagePixmap.setAlignment(Qt.AlignCenter) self.emptyPagePixmap.setObjectName("emptyPagePixmap") self.emptyPagePixmap.setPixmap( QPixmap(os.path.join(getConfig('ericPixDir'), 'eric.png'))) self.vboxlayout.addWidget(self.emptyPagePixmap) self.textLabel1 = QLabel(self.emptyPage) self.textLabel1.setAlignment(Qt.AlignCenter) self.textLabel1.setObjectName("textLabel1") self.vboxlayout.addWidget(self.textLabel1) spacerItem1 = QSpacerItem( 20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) self.vboxlayout.addItem(spacerItem1) self.configStack.addWidget(self.emptyPage) self.verticalLayout_2.addWidget(self.configSplitter) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons( QDialogButtonBox.Apply | QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.Reset) self.buttonBox.setObjectName("buttonBox") if not self.fromEric and \ self.displayMode == ConfigurationWidget.DefaultMode: self.buttonBox.button(QDialogButtonBox.Apply).hide() self.buttonBox.button(QDialogButtonBox.Apply).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Reset).setEnabled(False) self.verticalLayout_2.addWidget(self.buttonBox) self.setWindowTitle(self.tr("Preferences")) self.configList.header().hide() self.configList.header().setSortIndicator(0, Qt.AscendingOrder) self.configList.setSortingEnabled(True) self.textLabel1.setText( self.tr("Please select an entry of the list \n" "to display the configuration page.")) QMetaObject.connectSlotsByName(self) self.setTabOrder(self.configList, self.configStack) self.configStack.setCurrentWidget(self.emptyPage) self.configList.setFocus() def __searchTextChanged(self, text): """ Private slot to handle a change of the search text. @param text text to search for (string) """ self.__searchChildItems(self.configList.invisibleRootItem(), text) def __searchChildItems(self, parent, text): """ Private method to enable child items based on a search string. @param parent reference to the parent item (QTreeWidgetItem) @param text text to search for (string) @return flag indicating an enabled child item (boolean) """ childEnabled = False text = text.lower() for index in range(parent.childCount()): itm = parent.child(index) if itm.childCount() > 0: enable = self.__searchChildItems(itm, text) or \ text == "" or text in itm.text(0).lower() else: enable = text == "" or text in itm.text(0).lower() if enable: childEnabled = True itm.setDisabled(not enable) return childEnabled def __initLexers(self): """ Private method to initialize the dictionary of preferences lexers. """ import QScintilla.Lexers from .PreferencesLexer import PreferencesLexer, \ PreferencesLexerLanguageError self.lexers = {} for language in QScintilla.Lexers.getSupportedLanguages(): if language not in self.lexers: try: self.lexers[language] = PreferencesLexer(language, self) except PreferencesLexerLanguageError: pass def __importConfigurationPage(self, name): """ Private method to import a configuration page module. @param name name of the configuration page module (string) @return reference to the configuration page module """ modName = "Preferences.ConfigurationPages.{0}".format(name) try: mod = __import__(modName) components = modName.split('.') for comp in components[1:]: mod = getattr(mod, comp) return mod except ImportError: E5MessageBox.critical( self, self.tr("Configuration Page Error"), self.tr("""<p>The configuration page <b>{0}</b>""" """ could not be loaded.</p>""").format(name)) return None def __showConfigurationPage(self, itm, column): """ Private slot to show a selected configuration page. @param itm reference to the selected item (QTreeWidgetItem) @param column column that was selected (integer) (ignored) """ pageName = itm.getPageName() self.showConfigurationPageByName(pageName, setCurrent=False) def __initPage(self, pageData): """ Private method to initialize a configuration page. @param pageData data structure for the page to initialize @return reference to the initialized page """ page = None if isinstance(pageData[2], types.FunctionType): page = pageData[2](self) else: mod = self.__importConfigurationPage(pageData[2]) if mod: page = mod.create(self) if page is not None: self.configStack.addWidget(page) pageData[-1] = page try: page.setMode(self.displayMode) except AttributeError: pass return page def showConfigurationPageByName(self, pageName, setCurrent=True): """ Public slot to show a named configuration page. @param pageName name of the configuration page to show (string) @param setCurrent flag indicating to set the current item (boolean) """ if pageName == "empty" or pageName not in self.configItems: page = self.emptyPage else: pageData = self.configItems[pageName] if pageData[-1] is None and pageData[2] is not None: # the page was not loaded yet, create it page = self.__initPage(pageData) else: page = pageData[-1] if page is None: page = self.emptyPage elif setCurrent: items = self.configList.findItems( pageData[0], Qt.MatchFixedString | Qt.MatchRecursive) for item in items: if item.data(0, Qt.UserRole) == pageName: self.configList.setCurrentItem(item) self.configStack.setCurrentWidget(page) ssize = self.scrollArea.size() if self.scrollArea.horizontalScrollBar(): ssize.setHeight( ssize.height() - self.scrollArea.horizontalScrollBar().height() - 2) if self.scrollArea.verticalScrollBar(): ssize.setWidth( ssize.width() - self.scrollArea.verticalScrollBar().width() - 2) psize = page.minimumSizeHint() self.configStack.resize(max(ssize.width(), psize.width()), max(ssize.height(), psize.height())) if page != self.emptyPage: page.polishPage() self.buttonBox.button(QDialogButtonBox.Apply).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Reset).setEnabled(True) else: self.buttonBox.button(QDialogButtonBox.Apply).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Reset).setEnabled(False) # reset scrollbars for sb in [self.scrollArea.horizontalScrollBar(), self.scrollArea.verticalScrollBar()]: if sb: sb.setValue(0) self.__currentConfigurationPageName = pageName def getConfigurationPageName(self): """ Public method to get the page name of the current page. @return page name of the current page (string) """ return self.__currentConfigurationPageName def calledFromEric(self): """ Public method to check, if invoked from within eric. @return flag indicating invocation from within eric (boolean) """ return self.fromEric def getPage(self, pageName): """ Public method to get a reference to the named page. @param pageName name of the configuration page (string) @return reference to the page or None, indicating page was not loaded yet """ return self.configItems[pageName][-1] def getLexers(self): """ Public method to get a reference to the lexers dictionary. @return reference to the lexers dictionary """ return self.lexers def setPreferences(self): """ Public method called to store the selected values into the preferences storage. """ for key, pageData in list(self.configItems.items()): if pageData[-1]: pageData[-1].save() # page was loaded (and possibly modified) QApplication.processEvents() # ensure HMI is responsive def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Apply): self.on_applyButton_clicked() elif button == self.buttonBox.button(QDialogButtonBox.Reset): self.on_resetButton_clicked() @pyqtSlot() def on_applyButton_clicked(self): """ Private slot called to apply the settings of the current page. """ if self.configStack.currentWidget() != self.emptyPage: page = self.configStack.currentWidget() savedState = page.saveState() page.save() self.preferencesChanged.emit() if savedState is not None: page.setState(savedState) page.polishPage() @pyqtSlot() def on_resetButton_clicked(self): """ Private slot called to reset the settings of the current page. """ if self.configStack.currentWidget() != self.emptyPage: currentPage = self.configStack.currentWidget() savedState = currentPage.saveState() pageName = self.configList.currentItem().getPageName() self.configStack.removeWidget(currentPage) if pageName == "editorHighlightingStylesPage": self.__initLexers() self.configItems[pageName][-1] = None self.showConfigurationPageByName(pageName) if savedState is not None: self.configStack.currentWidget().setState(savedState) def getExpandedEntries(self): """ Public method to get a list of expanded entries. @return list of expanded entries (list of string) """ return self.__expandedEntries @pyqtSlot(QTreeWidgetItem) def on_configList_itemCollapsed(self, item): """ Private slot handling a list entry being collapsed. @param item reference to the collapsed item (QTreeWidgetItem) """ pageName = item.data(0, Qt.UserRole) if pageName in self.__expandedEntries: self.__expandedEntries.remove(pageName) @pyqtSlot(QTreeWidgetItem) def on_configList_itemExpanded(self, item): """ Private slot handling a list entry being expanded. @param item reference to the expanded item (QTreeWidgetItem) """ pageName = item.data(0, Qt.UserRole) if pageName not in self.__expandedEntries: self.__expandedEntries.append(pageName)