Пример #1
0
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)
Пример #2
0
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)
Пример #3
0
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)
Пример #4
0
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)
Пример #5
0
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)
Пример #6
0
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
Пример #7
0
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)
Пример #8
0
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)
Пример #9
0
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()
Пример #10
0
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
Пример #11
0
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')
Пример #12
0
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()
Пример #13
0
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
Пример #14
0
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)
Пример #15
0
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)
Пример #16
0
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
Пример #17
0
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()
Пример #18
0
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()
Пример #19
0
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
Пример #20
0
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)
Пример #21
0
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()
Пример #22
0
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
Пример #23
0
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())
Пример #24
0
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()
Пример #25
0
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)
Пример #26
0
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()
Пример #27
0
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)
Пример #28
0
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)
Пример #29
0
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)])
Пример #30
0
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))
Пример #31
0
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())
Пример #32
0
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)
Пример #33
0
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)
Пример #34
0
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)
Пример #35
0
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")
Пример #36
0
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)])
Пример #37
0
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)
Пример #38
0
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)
Пример #39
0
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
Пример #41
-1
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)