Esempio n. 1
0
class Exercice(Panel_API_graphique):

    titre = u"Exercice" # À adapter pour chaque module

    def __init__(self, *args, **kw):
        Panel_API_graphique.__init__(self, *args, **kw)

        self.entrees = QVBoxLayout()
        self.entrees.addSpacing(30)

        self.panneau = QLabel('')
        self.entrees.addWidget(self.panneau)

        self.entrees.addStretch()
        self.felicitations = QLabel('')
        self.entrees.addWidget(self.felicitations)

        self.entrees.addSpacing(30)
        self.btn_niveau = QPushButton(u"Niveau suivant", self)
        self.btn_niveau.clicked.connect(self.niveau_suivant)
        self.entrees.addWidget(self.btn_niveau)
        self.entrees.addSpacing(50)

        self.sizer = QHBoxLayout()
        self.sizer.addWidget(self.canvas, 1)
        self.sizer.addLayout(self.entrees, 0.2)
        self.finaliser(contenu=self.sizer)

        self.reinitialiser()


    def reinitialiser(self):
        u"""Revient au 1er niveau, et remet tous les réglages par défaut.

        Chaque niveau peut bien sûr modifier ces réglages.

        Quelques remarques:
        * le clic droit est désactivé, car il permet d'obtenir la réponse
          en éditant les propriétés du champ de texte.
        * l'édition des champs/textes avec [Entrée] est désactivée
          (car cela s'est avéré perturber les élèves).
        """
        # Ne pas éditer les champs/textes avec [Entrée]
        self.canvas.editeur.active = False
        # Ne pas éditer les objets par un clic droit
        self.canvas.edition_par_clic_droit = False

        # Réglages par défaut
        self.canvas.fixe = True
        self.canvas.afficher_axes = False
        self.canvas.afficher_quadrillage = False
        self.afficher_barre_outils(False)
        self.canvas.ratio = None

        # Réinitialisation du score et retour au niveau 1
        if param.debug:
            print(u'Module %s: réinitialisation...' % self.nom)
        self.score = 0
        self.niveau = 0
        self.erreurs = 0
        self.niveau_suivant()



    def niveau_suivant(self, niveau=None):
        # On ferme toutes les feuilles ouvertes (inutile en principe),
        # et on en ouvre une nouvelle.
        self.fermer_feuilles()
        # Et on change de niveau...
        if niveau in (None, False):
            # None ou False (False est renvoyé par Qt via QAbstractBouton.clicked)
            self.niveau += 1
        else:
            self.niveau = niveau
        if param.debug:
            print("== Niveau %s ==" % self.niveau)
        getattr(self, 'niveau%s' % self.niveau)()

        self.btn_niveau.setEnabled(False)
        self.felicitations.setStyleSheet(
            """QLabel {background-color: white; padding: 5px; border-radius: 5px;
            color:white;}""")
        self.update_panneau()

    n = niveau_suivant


    def update_panneau(self):
        self.panneau.setStyleSheet(
            """QLabel { padding: 10px; border-width: 2px; border-style:solid;
            border-radius: 5px; border-color:%s; background-color: %s }"""
            %(QColor(30, 144, 255).name(), QColor(176, 226, 255).name())
                        )
        self.panneau.setText((u"<p><b><i>Niveau :</i> %s</b></p>" % self.niveau) +
                                 (u"<p><b><i>Points :</i> %s</b></p>" % self.score) +
                                 (u"<p><i>Erreurs :</i> %s</p>" % self.erreurs))
        champs = self.feuille_actuelle.objets.lister(type=Champ)
        if champs and all(obj.correct for obj in champs):
            if hasattr(self, 'niveau' + str(self.niveau + 1)):
                self.btn_niveau.setEnabled(True)
                self.btn_niveau.setFocus(True)
                self.felicitations.setText(u'<p><b>Félicitations !</b></p>' +
                                           u'<p>Passer au niveau %s</p>' %(self.niveau + 1))
                self.felicitations.setStyleSheet(
                    """QLabel {background-color: %s; padding: 5px;
                       border-radius: 5px;
                       color:white;}""" %QColor(255, 153, 0).name())

            else:
                self.felicitations.setText(u'<p><b>Félicitations !</b></p>' +
                                           u'<p>Dernier niveau terminé !</p>')
                self.felicitations.setStyleSheet(
                    """QLabel {background-color: %s; padding: 5px; border-radius: 5px;
                    color:white;}""" %QColor(102, 205, 0).name())

    ##def _sauvegarder(self, fgeo, feuille = None):
        ##Panel_API_graphique._sauvegarder(self, fgeo, feuille)
        ##fgeo.contenu[u"niveau"] = [str(self.niveau)]
        ##fgeo.contenu[u"expression"] = [self.raw_expression]
        ##fgeo.contenu[u"score"] = [str(self.score)]
        ##fgeo.contenu[u"erreurs"] = [str(self.erreurs)]
##
    def _ouvrir(self, fgeo):
        pass
        ### Il ne doit y avoir qu'une seule feuille ouverte à la fois.
        ### XXX: intégrer cette fonctionnalité directement au Panel.
        ##self.fermer_feuilles()
        ##Panel_API_graphique._ouvrir(self, fgeo)
        ##if fgeo.contenu.has_key(u"expression"):
            ##self.generer_expression(expr=fgeo.contenu[u"expression"][0])
            ##self.dessiner_tableau()
        ##if fgeo.contenu.has_key(u"niveau"):
            ##self.niveau = int(fgeo.contenu[u"niveau"][0])
        ##if fgeo.contenu.has_key(u"score"):
            ##self.score = int(fgeo.contenu[u"score"][0])
        ##if fgeo.contenu.has_key(u"erreurs"):
            ##self.erreurs = int(fgeo.contenu[u"erreurs"][0])
        ##self.update_panneau()

    ##def _affiche(self):
        ##self.dessiner_tableau()


    # --------------------------------
    # Génération de nombres aléatoires
    # --------------------------------

    @staticmethod
    def signe():
        return 2*randint(0, 1) - 1

    def naturel(self, n=15):
        u'''Retourne un entier entre 2 et `n`.'''
        return randint(2, n)

    def relatif(self, n=15):
        u'''Retourne un entier entre -`n` et -2, ou entre 2 et `n`.'''
        # signe: 1 ou -1
        return self.signe()*self.naturel(n)

    def decimal(self, chiffres=2):
        u'''Retourne un nombre décimal, `chiffres` est le nombre de chiffres.'''
        return  S('%s.%s' % (self.relatif(), self.naturel()))

    def rationnel(self, n=7):
        u'''Retourne un quotient d'entiers.'''
        while True:
            p = self.naturel(n)
            q = self.naturel(n)
            if p%q:
                break
        return self.signe()*S(p)/S(q)

    def couple(self, m=7, n=7):
        u"""Retourne un couple d'entiers relatifs."""
        return self.relatif(m), self.relatif(n)

    def autocompleter(self):
        u"""Compléter automatiquement avec les bonnes réponses
        pour pouvoir passer au niveau suivant.
        Essentiellement pour déboguer."""
        ##if self.btn_niveau.isEnabled():
            ##self.niveau_suivant()
        self.btn_niveau.click()
        for t in self.feuille_actuelle.objets.lister(type=Champ):
            t.texte = t.style('attendu')
            t.style(color='g')
        ##self.parent.parent.ligne_commande.setFocus()

    a = property(autocompleter)

    def bonus(self, expr):
        u"""À surclasser pour accorder un bonus si l'expression vérifie
        certaines conditions.
        Par exemple, on peut tester que le résultat est bien simplifié."""
        return False

    def compter_points(self, **kw):
        if 'correct' in kw and 'correct_old' in kw and 'champ' in kw:
            champ = kw['champ']
            if kw['correct']:
                if not kw['correct_old']:
                    if not champ.style('choix'):
                        # C'est plus dur s'il n'y a pas de choix proposé.
                        # On accorde une bonification si le résultat est
                        # un minimum simplifié.
                        if self.bonus(champ.label()):
                            self.score += 3
                        self.score += 1
                    self.score += 1
            else:
                self.score -= 1
                self.erreurs += 1
        if all(obj.correct for obj in self.feuille_actuelle.objets.lister(type=Champ)):
            self.score += 10*(self.niveau + 1)
        self.update_panneau()
Esempio n. 2
0
class ContentsBrowser(QDialog):
    """
    Displays the contents of a DVID server, listing all repos and the volumes/nodes within each repo.
    The user's selected repo, volume, and node can be accessed via the `get_selection()` method.
    
    If the dialog is constructed with mode='specify_new', then the user is asked to provide a new data name, 
    and choose the repo and node to which it will belong. 
    
    **TODO:**

    * Show more details in node list (e.g. date modified, parents, children)
    * Gray-out nodes that aren't "open" for adding new volumes
    """
    def __init__(self,
                 suggested_hostnames,
                 default_nodes=None,
                 mode='select_existing',
                 selectable_type=VOXEL_TYPENAMES,
                 parent=None):
        """
        Constructor.
        
        suggested_hostnames: A list of hostnames to suggest to the user, e.g. ["localhost:8000"]
        default_nodes: A dict of {hostname : uuid} specifying which node to auto-select for each possible host.
        mode: Either 'select_existing' or 'specify_new'
        parent: The parent widget.
        """
        assert mode in ('select_existing',
                        'specify_new'), "Invalid mode: {}".format(mode)
        super(ContentsBrowser, self).__init__(parent)
        self._suggested_hostnames = suggested_hostnames
        self._default_nodes = default_nodes
        self._mode = mode
        self._current_repo = None
        self._repos_info = None
        self._hostname = None

        if isinstance(selectable_type, collections.Iterable):
            self._selectable_types = selectable_type
        else:
            assert isinstance(
                selectable_type,
                str), "selectable_type must be str or list-of-str"
            self._selectable_types = (selectable_type, )

        # Create the UI
        self._init_layout()

    VolumeSelection = collections.namedtuple(
        "VolumeSelection", "hostname repo_uuid data_name node_uuid typename")

    def get_selection(self):
        """
        Get the user's current (or final) selection.
        Returns a VolumeSelection tuple.
        """
        # Convert to str (Qt tends to convert these into unicode)
        node_uuid = str(self._get_selected_node())
        repo_uuid, data_name, typename = map(str, self._get_selected_data())

        if self._mode == "specify_new":
            data_name = str(self._new_data_edit.text())

        return ContentsBrowser.VolumeSelection(str(self._hostname), repo_uuid,
                                               data_name, node_uuid, typename)

    def _init_layout(self):
        """
        Create the GUI widgets (but leave them empty).
        """
        hostname_combobox = QComboBox(parent=self)
        self._hostname_combobox = hostname_combobox
        hostname_combobox.setEditable(True)
        hostname_combobox.setSizePolicy(QSizePolicy.Expanding,
                                        QSizePolicy.Maximum)
        for hostname in self._suggested_hostnames:
            hostname_combobox.addItem(hostname)

        # EventFilter is installed after everything else is initialized. (See below.)
        #hostname_combobox.installEventFilter(self)

        self._connect_button = QPushButton("Connect",
                                           parent=self,
                                           clicked=self._handle_new_hostname)

        hostname_layout = QHBoxLayout()
        hostname_layout.addWidget(hostname_combobox)
        hostname_layout.addWidget(self._connect_button)

        hostinfo_table = QTableWidget()
        hostinfo_table.setColumnCount(len(SERVER_INFO_FIELDS))
        hostinfo_table.setHorizontalHeaderLabels(SERVER_INFO_FIELDS)
        hostinfo_table.horizontalHeader().setVisible(True)
        hostinfo_table.verticalHeader().setVisible(False)
        hostinfo_table.setRowCount(1)
        hostinfo_table.setItem(0, 0, QTableWidgetItem("Placeholder"))
        hostinfo_table.setVisible(False)
        hostinfo_table.resizeRowsToContents()
        hostinfo_table.horizontalHeader().setStretchLastSection(True)
        table_height = hostinfo_table.verticalHeader().sectionSize(
            0) + hostinfo_table.rowHeight(0)
        hostinfo_table.resize(QSize(hostinfo_table.width(), table_height))
        hostinfo_table.setMaximumSize(QSize(1000, table_height))
        hostinfo_table.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)

        host_layout = QVBoxLayout()
        host_layout.addLayout(hostname_layout)
        host_layout.addWidget(hostinfo_table)

        host_groupbox = QGroupBox("DVID Host", parent=self)
        host_groupbox.setLayout(host_layout)
        host_groupbox.setSizePolicy(QSizePolicy.Preferred,
                                    QSizePolicy.Preferred)

        repo_treewidget = QTreeWidget(parent=self)
        repo_treewidget.setHeaderLabels(
            TREEVIEW_COLUMNS)  # TODO: Add type, shape, axes, etc.
        repo_treewidget.setSizePolicy(QSizePolicy.Preferred,
                                      QSizePolicy.Preferred)
        repo_treewidget.itemSelectionChanged.connect(
            self._handle_data_selection)

        data_layout = QVBoxLayout()
        data_layout.addWidget(repo_treewidget)
        data_groupbox = QGroupBox("Data Volumes", parent=self)
        data_groupbox.setLayout(data_layout)

        node_listwidget = QListWidget(parent=self)
        node_listwidget.setSizePolicy(QSizePolicy.Preferred,
                                      QSizePolicy.Preferred)
        node_listwidget.itemSelectionChanged.connect(self._update_status)

        node_layout = QVBoxLayout()
        node_layout.addWidget(node_listwidget)
        node_groupbox = QGroupBox("Nodes", parent=self)
        node_groupbox.setLayout(node_layout)

        new_data_edit = QLineEdit(parent=self)
        new_data_edit.textEdited.connect(self._update_status)
        full_url_label = QLabel(parent=self)
        full_url_label.setSizePolicy(QSizePolicy.Preferred,
                                     QSizePolicy.Maximum)
        text_flags = full_url_label.textInteractionFlags()
        full_url_label.setTextInteractionFlags(text_flags
                                               | Qt.TextSelectableByMouse)

        new_data_layout = QVBoxLayout()
        new_data_layout.addWidget(new_data_edit)
        new_data_groupbox = QGroupBox("New Data Volume", parent=self)
        new_data_groupbox.setLayout(new_data_layout)
        new_data_groupbox.setSizePolicy(QSizePolicy.Preferred,
                                        QSizePolicy.Maximum)

        buttonbox = QDialogButtonBox(Qt.Horizontal, parent=self)
        buttonbox.setStandardButtons(QDialogButtonBox.Ok
                                     | QDialogButtonBox.Cancel)
        buttonbox.accepted.connect(self.accept)
        buttonbox.rejected.connect(self.reject)
        buttonbox.button(QDialogButtonBox.Ok).setEnabled(False)

        layout = QVBoxLayout()
        layout.addWidget(host_groupbox)
        layout.addWidget(data_groupbox)
        layout.addWidget(node_groupbox)
        if self._mode == "specify_new":
            layout.addWidget(new_data_groupbox)
        else:
            new_data_groupbox.hide()
        layout.addWidget(full_url_label)
        layout.addWidget(buttonbox)

        # Stretch factors
        layout.setStretchFactor(data_groupbox, 3)
        layout.setStretchFactor(node_groupbox, 1)

        self.setLayout(layout)
        self.setWindowTitle("Select DVID Volume")
        self.resize(1000, 1000)

        # Initially disabled
        data_groupbox.setEnabled(False)
        node_groupbox.setEnabled(False)
        new_data_groupbox.setEnabled(False)

        # Set tab order
        self.setTabOrder(hostname_combobox, repo_treewidget)
        self.setTabOrder(repo_treewidget, node_listwidget)
        self.setTabOrder(node_listwidget, buttonbox)

        # Save instance members
        self._hostinfo_table = hostinfo_table
        self._data_groupbox = data_groupbox
        self._node_groupbox = node_groupbox
        self._new_data_groupbox = new_data_groupbox
        self._repo_treewidget = repo_treewidget
        self._node_listwidget = node_listwidget
        self._new_data_edit = new_data_edit
        self._full_url_label = full_url_label
        self._buttonbox = buttonbox

        # Finally install eventfilter (after everything is initialized)
        hostname_combobox.installEventFilter(self)

    def sizeHint(self):
        return QSize(1000, 1000)

    def eventFilter(self, watched, event):
        """
        When the user presses the 'Enter' key, auto-click 'Connect'.
        If we've already connected, auto-click 'OK'.
        """
        if watched == self._hostname_combobox \
        and event.type() == QEvent.KeyPress \
        and ( event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter):
            if self._hostname:
                self._buttonbox.button(QDialogButtonBox.Ok).click()
            else:
                self._connect_button.click()
            return True
        return False

    def showEvent(self, event):
        """
        Raise the window when it is shown.
        For some reason, that doesn't happen automatically if this widget is also the main window.
        """
        super(ContentsBrowser, self).showEvent(event)
        self.raise_()

    def _handle_new_hostname(self):
        """
        Called by 'Connect' button.
        Connect to the server, download the server info and repo info,
        and populate the GUI widgets with the data.
        """
        new_hostname = str(self._hostname_combobox.currentText())
        if '://' in new_hostname:
            new_hostname = new_hostname.split('://')[1]

        error_msg = None
        self._server_info = None
        self._repos_info = None
        self._current_repo = None
        self._hostname = None

        try:
            # Query the server
            connection = DVIDConnection(new_hostname)
            self._server_info = ContentsBrowser._get_server_info(connection)
            self._repos_info = ContentsBrowser._get_repos_info(connection)
            self._hostname = new_hostname
            self._connection = connection
        except DVIDException as ex:
            error_msg = "libdvid.DVIDException: {}".format(ex.message)
        except ErrMsg as ex:
            error_msg = "libdvid.ErrMsg: {}".format(ex.message)

        if error_msg:
            QMessageBox.critical(self, "Connection Error", error_msg)
            self._populate_node_list(None)
        else:
            self._connect_button.setEnabled(False)
            self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(True)

        enable_contents = self._repos_info is not None
        self._data_groupbox.setEnabled(enable_contents)
        self._node_groupbox.setEnabled(enable_contents)
        self._new_data_groupbox.setEnabled(enable_contents)

        self._populate_hostinfo_table()
        self._populate_repo_tree()

    @classmethod
    def _get_server_info(cls, connection):
        status, body, _error_message = connection.make_request(
            "/server/info", ConnectionMethod.GET)
        server_info = json.loads(body)
        return server_info

    @classmethod
    def _get_repos_info(cls, connection):
        status, body, _error_message = connection.make_request(
            "/repos/info", ConnectionMethod.GET)
        repos_info = json.loads(body)

        # Discard uuids with 'null' content (I don't know why they sometimes exist...)
        repos_info = [
            uuid_repo_info for uuid_repo_info in list(repos_info.items())
            if uuid_repo_info[1]
        ]
        return collections.OrderedDict(sorted(repos_info))

    def _populate_hostinfo_table(self):
        self._hostinfo_table.setVisible(self._server_info is not None)
        if not self._server_info:
            return

        for column_index, fieldname in enumerate(SERVER_INFO_FIELDS):
            try:
                field = self._server_info[fieldname]
            except KeyError:
                field = "<UNDEFINED>"
            item = QTableWidgetItem(field)
            flags = item.flags()
            flags &= ~Qt.ItemIsSelectable
            flags &= ~Qt.ItemIsEditable
            item.setFlags(flags)
            self._hostinfo_table.setItem(0, column_index, item)
        self._hostinfo_table.resizeColumnsToContents()
        self._hostinfo_table.horizontalHeader().setStretchLastSection(
            True)  # Force refresh of last column.

    def _populate_repo_tree(self):
        """
        Initialize the tree widget of repos and volumes.
        """
        self._repo_treewidget.clear()

        if self._repos_info is None:
            return

        for repo_uuid, repo_info in sorted(self._repos_info.items()):
            if repo_info is None:
                continue
            repo_column_dict = collections.defaultdict(str)
            repo_column_dict["Alias"] = repo_info["Alias"]
            repo_column_dict["Details"] = "Created: " + repo_info["Created"]
            repo_column_dict["UUID"] = repo_uuid
            repo_column_values = [
                repo_column_dict[k] for k in TREEVIEW_COLUMNS
            ]
            repo_item = QTreeWidgetItem(self._repo_treewidget,
                                        QStringList(repo_column_values))
            repo_item.setData(0, Qt.UserRole, (repo_uuid, "", ""))
            for data_name, data_info in repo_info["DataInstances"].items():
                data_instance_dict = collections.defaultdict(str)
                data_instance_dict["Alias"] = data_name
                typename = data_info["Base"]["TypeName"]
                data_instance_dict["TypeName"] = typename

                is_voxels = (typename in VOXEL_TYPENAMES)
                if is_voxels:
                    start_coord = data_info["Extended"]["MinPoint"]
                    if start_coord:
                        start_coord = tuple(start_coord)
                    stop_coord = data_info["Extended"]["MaxPoint"]
                    if stop_coord:
                        stop_coord = tuple(x + 1 for x in stop_coord)
                    if start_coord and stop_coord:
                        shape = tuple(b - a
                                      for a, b in zip(start_coord, stop_coord))
                    else:
                        shape = None
                    data_instance_dict[
                        "Details"] = "Size={} | Start={} | Stop={}".format(
                            shape, start_coord, stop_coord)

                data_column_values = [
                    data_instance_dict[k] for k in TREEVIEW_COLUMNS
                ]
                data_item = QTreeWidgetItem(repo_item, data_column_values)
                data_item.setData(0, Qt.UserRole,
                                  (repo_uuid, data_name, typename))

                # If we're in specify_new mode, only the
                # repo parent items are selectable.
                # Also, non-volume items aren't selectable.
                if self._mode == 'specify_new' or typename not in self._selectable_types:
                    flags = data_item.flags()
                    flags &= ~Qt.ItemIsSelectable
                    flags &= ~Qt.ItemIsEnabled
                    data_item.setFlags(flags)

        self._repo_treewidget.collapseAll()
        self._repo_treewidget.setSortingEnabled(True)

        if self._hostname in self._default_nodes:
            self._select_node_uuid(self._default_nodes[self._hostname])

        self._repo_treewidget.resizeColumnToContents(0)
        self._repo_treewidget.setFocus()

    def _handle_data_selection(self):
        """
        When the user clicks a new data item, respond by updating the node list.
        """
        selected_items = self._repo_treewidget.selectedItems()
        if not selected_items:
            return None
        item = selected_items[0]
        item_data = item.data(0, Qt.UserRole).toPyObject()
        if not item_data:
            return
        repo_uuid, data_name, typename = item_data
        if self._current_repo != repo_uuid:
            self._populate_node_list(repo_uuid)

        self._update_status()

    def _populate_node_list(self, repo_uuid):
        """
        Replace the contents of the node list widget 
        to show all the nodes for the currently selected repo.
        """
        self._node_listwidget.clear()

        if self._repos_info is None or repo_uuid is None:
            return

        # For now, we simply show the nodes in sorted order, without respect to dag order
        all_uuids = sorted(self._repos_info[repo_uuid]["DAG"]["Nodes"].keys())
        for node_uuid in all_uuids:
            node_item = QListWidgetItem(node_uuid,
                                        parent=self._node_listwidget)
            node_item.setData(Qt.UserRole, node_uuid)
        self._current_repo = repo_uuid

        # Select the last one by default.
        last_row = self._node_listwidget.count() - 1
        last_item = self._node_listwidget.item(last_row)
        self._node_listwidget.setCurrentItem(last_item)
        self._update_status()

    def _get_selected_node(self):
        """
        Return the currently selected node uuid.
        """
        selected_items = self._node_listwidget.selectedItems()
        if not selected_items:
            return None
        selected_node_item = selected_items[0]
        node_item_data = selected_node_item.data(Qt.UserRole)
        return str(node_item_data.toString())

    def _get_selected_data(self):
        """
        Return the repo, data name, and type of the currently selected data volume (or ROI).
        """
        selected_items = self._repo_treewidget.selectedItems()
        if not selected_items:
            return None, None
        selected_data_item = selected_items[0]
        data_item_data = selected_data_item.data(0, Qt.UserRole).toPyObject()
        if selected_data_item:
            repo_uuid, data_name, typename = data_item_data
        else:
            repo_uuid = data_name = typename = None
        return repo_uuid, data_name, typename

    def _select_node_uuid(self, node_uuid):
        """
        Locate the repo that owns this uuid, and select it in the GUI.
        If the uuid can't be found, do nothing.
        """
        def select_repotree_item(repo_uuid):
            for row in range(self._repo_treewidget.topLevelItemCount()):
                repo_item = self._repo_treewidget.topLevelItem(row)
                if repo_uuid == repo_item.data(0, Qt.UserRole).toPyObject()[0]:
                    self._repo_treewidget.setCurrentItem(repo_item)
                    repo_item.setExpanded(True)
                    self._repo_treewidget.scrollTo(
                        self._repo_treewidget.selectedIndexes()[0],
                        QTreeWidget.PositionAtCenter)
                    break

        def select_nodelist_item(node_uuid):
            for row in range(self._node_listwidget.count()):
                item = self._node_listwidget.item(row)
                if node_uuid == item.data(Qt.UserRole).toPyObject():
                    self._node_listwidget.setCurrentItem(item)
                    break

        for repo_uuid, repo_info in sorted(self._repos_info.items()):
            if node_uuid in repo_info["DAG"]["Nodes"].keys():
                # Select the right repo parent item
                select_repotree_item(repo_uuid)

                # Select the right row in the node list
                # (The node list was automatically updated when the repo selection changed, above.)
                select_nodelist_item(node_uuid)
                break

    def _update_status(self):
        """
        Update the path label to reflect the user's currently selected uuid and new volume name.
        """
        hostname, repo_uuid, dataname, node_uuid, typename = self.get_selection(
        )
        full_path = "http://{hostname}/api/node/{uuid}/{dataname}"\
                    "".format( hostname=self._hostname, uuid=node_uuid, dataname=dataname )
        self._full_url_label.setText(full_path)

        ok_button = self._buttonbox.button(QDialogButtonBox.Ok)
        ok_button.setEnabled(dataname != "")
Esempio n. 3
0
class ContentsBrowser(QDialog):
    """
    Displays the contents of a DVID server, listing all datasets and the volumes/nodes within each dataset.
    The user's selected dataset, volume, and node can be accessed via the `get_selection()` method.
    
    If the dialog is constructed with mode='specify_new', then the user is asked to provide a new data name, 
    and choose the dataset and node to which it will belong. 
    
    **TODO:**

    * Show more details in dataset list (e.g. shape, axes, pixel type)
    * Show more details in node list (e.g. date modified, parents, children)
    * Gray-out nodes that aren't "open" for adding new volumes
    """
    def __init__(self,
                 suggested_hostnames,
                 mode='select_existing',
                 parent=None):
        """
        Constructor.
        
        suggested_hostnames: A list of hostnames to suggest to the user, e.g. ["localhost:8000"]
        mode: Either 'select_existing' or 'specify_new'
        parent: The parent widget.
        """
        super(ContentsBrowser, self).__init__(parent)
        self._suggested_hostnames = suggested_hostnames
        self._mode = mode
        self._current_dset = None
        self._repos_info = None
        self._hostname = None

        # Create the UI
        self._init_layout()

    VolumeSelection = collections.namedtuple(
        "VolumeSelection", "hostname dataset_uuid data_name node_uuid")

    def get_selection(self):
        """
        Get the user's current (or final) selection.
        Returns a VolumeSelection tuple.
        """
        node_uuid = self._get_selected_node()
        dset_uuid, data_name = self._get_selected_data()

        if self._mode == "specify_new":
            data_name = str(self._new_data_edit.text())

        return ContentsBrowser.VolumeSelection(self._hostname, dset_uuid,
                                               data_name, node_uuid)

    def _init_layout(self):
        """
        Create the GUI widgets (but leave them empty).
        """
        hostname_combobox = QComboBox(parent=self)
        self._hostname_combobox = hostname_combobox
        hostname_combobox.setEditable(True)
        hostname_combobox.setSizePolicy(QSizePolicy.Expanding,
                                        QSizePolicy.Maximum)
        hostname_combobox.installEventFilter(self)
        for hostname in self._suggested_hostnames:
            hostname_combobox.addItem(hostname)

        self._connect_button = QPushButton("Connect",
                                           parent=self,
                                           clicked=self._handle_new_hostname)

        hostname_layout = QHBoxLayout()
        hostname_layout.addWidget(hostname_combobox)
        hostname_layout.addWidget(self._connect_button)

        hostname_groupbox = QGroupBox("DVID Host", parent=self)
        hostname_groupbox.setLayout(hostname_layout)
        hostname_groupbox.setSizePolicy(QSizePolicy.Preferred,
                                        QSizePolicy.Maximum)

        data_treewidget = QTreeWidget(parent=self)
        data_treewidget.setHeaderLabels(
            ["Data"])  # TODO: Add type, shape, axes, etc.
        data_treewidget.setSizePolicy(QSizePolicy.Preferred,
                                      QSizePolicy.Preferred)
        data_treewidget.itemSelectionChanged.connect(
            self._handle_data_selection)

        data_layout = QVBoxLayout()
        data_layout.addWidget(data_treewidget)
        data_groupbox = QGroupBox("Data Volumes", parent=self)
        data_groupbox.setLayout(data_layout)

        node_listwidget = QListWidget(parent=self)
        node_listwidget.setSizePolicy(QSizePolicy.Preferred,
                                      QSizePolicy.Preferred)
        node_listwidget.itemSelectionChanged.connect(self._update_display)

        node_layout = QVBoxLayout()
        node_layout.addWidget(node_listwidget)
        node_groupbox = QGroupBox("Nodes", parent=self)
        node_groupbox.setLayout(node_layout)

        new_data_edit = QLineEdit(parent=self)
        new_data_edit.textEdited.connect(self._update_display)
        full_url_label = QLabel(parent=self)
        full_url_label.setSizePolicy(QSizePolicy.Preferred,
                                     QSizePolicy.Maximum)
        text_flags = full_url_label.textInteractionFlags()
        full_url_label.setTextInteractionFlags(text_flags
                                               | Qt.TextSelectableByMouse)

        new_data_layout = QVBoxLayout()
        new_data_layout.addWidget(new_data_edit)
        new_data_groupbox = QGroupBox("New Data Volume", parent=self)
        new_data_groupbox.setLayout(new_data_layout)
        new_data_groupbox.setSizePolicy(QSizePolicy.Preferred,
                                        QSizePolicy.Maximum)

        buttonbox = QDialogButtonBox(Qt.Horizontal, parent=self)
        buttonbox.setStandardButtons(QDialogButtonBox.Ok
                                     | QDialogButtonBox.Cancel)
        buttonbox.accepted.connect(self.accept)
        buttonbox.rejected.connect(self.reject)
        buttonbox.button(QDialogButtonBox.Ok).setEnabled(False)

        layout = QVBoxLayout()
        layout.addWidget(hostname_groupbox)
        layout.addWidget(data_groupbox)
        layout.addWidget(node_groupbox)
        if self._mode == "specify_new":
            layout.addWidget(new_data_groupbox)
        else:
            new_data_groupbox.hide()
        layout.addWidget(full_url_label)
        layout.addWidget(buttonbox)

        # Stretch factors
        layout.setStretchFactor(data_groupbox, 3)
        layout.setStretchFactor(node_groupbox, 1)

        self.setLayout(layout)
        self.setWindowTitle("Select DVID Volume")

        # Initially disabled
        data_groupbox.setEnabled(False)
        node_groupbox.setEnabled(False)
        new_data_groupbox.setEnabled(False)

        # Save instance members
        self._data_groupbox = data_groupbox
        self._node_groupbox = node_groupbox
        self._new_data_groupbox = new_data_groupbox
        self._data_treewidget = data_treewidget
        self._node_listwidget = node_listwidget
        self._new_data_edit = new_data_edit
        self._full_url_label = full_url_label
        self._buttonbox = buttonbox

    def sizeHint(self):
        return QSize(700, 500)

    def eventFilter(self, watched, event):
        if watched == self._hostname_combobox \
        and event.type() == QEvent.KeyPress \
        and ( event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter):
            self._connect_button.click()
            return True
        return False

    def showEvent(self, event):
        """
        Raise the window when it is shown.
        For some reason, that doesn't happen automatically if this widget is also the main window.
        """
        super(ContentsBrowser, self).showEvent(event)
        self.raise_()

    def _handle_new_hostname(self):
        new_hostname = str(self._hostname_combobox.currentText())
        if '://' in new_hostname:
            new_hostname = new_hostname.split('://')[1]

        error_msg = None
        self._repos_info = None
        self._current_dset = None
        self._hostname = None
        try:
            # Query the server
            connection = httplib.HTTPConnection(new_hostname)
            self._repos_info = pydvid.general.get_repos_info(connection)
            self._hostname = new_hostname
            self._connection = connection
        except socket.error as ex:
            error_msg = "Socket Error: {} (Error {})".format(
                ex.args[1], ex.args[0])
        except httplib.HTTPException as ex:
            error_msg = "HTTP Error: {}".format(ex.args[0])

        if error_msg:
            QMessageBox.critical(self, "Connection Error", error_msg)
            self._populate_datasets_tree()
            self._populate_node_list(-1)
        else:
            self._connect_button.setEnabled(False)
            self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(True)

        enable_contents = self._repos_info is not None
        self._data_groupbox.setEnabled(enable_contents)
        self._node_groupbox.setEnabled(enable_contents)
        self._new_data_groupbox.setEnabled(enable_contents)

        self._populate_datasets_tree()

    def _populate_datasets_tree(self):
        """
        Initialize the tree widget of datasets and volumes.
        """
        self._data_treewidget.clear()

        if self._repos_info is None:
            return

        for dset_uuid, dset_info in sorted(self._repos_info.items()):
            dset_item = QTreeWidgetItem(self._data_treewidget,
                                        QStringList(dset_uuid))
            dset_item.setData(0, Qt.UserRole, (dset_uuid, ""))
            for data_name in dset_info["DataInstances"].keys():
                data_item = QTreeWidgetItem(dset_item, QStringList(data_name))
                data_item.setData(0, Qt.UserRole, (dset_uuid, data_name))
                if self._mode == 'specify_new':
                    # If we're in specify_new mode, only the dataset parent items are selectable.
                    flags = data_item.flags()
                    flags &= ~Qt.ItemIsSelectable
                    flags &= ~Qt.ItemIsEnabled
                    data_item.setFlags(flags)

        # Expand everything
        self._data_treewidget.expandAll()

        # Select the first item by default.
        if self._mode == "select_existing":
            first_item = self._data_treewidget.topLevelItem(0).child(0)
        else:
            first_item = self._data_treewidget.topLevelItem(0)
        self._data_treewidget.setCurrentItem(first_item, 0)

    def _handle_data_selection(self):
        """
        When the user clicks a new data item, respond by updating the node list.
        """
        selected_items = self._data_treewidget.selectedItems()
        if not selected_items:
            return None
        item = selected_items[0]
        item_data = item.data(0, Qt.UserRole).toPyObject()
        if not item_data:
            return
        dset_uuid, data_name = item_data
        if self._current_dset != dset_uuid:
            self._populate_node_list(dset_uuid)

        self._update_display()

    def _populate_node_list(self, dataset_uuid):
        """
        Replace the contents of the node list widget 
        to show all the nodes for the currently selected dataset.
        """
        self._node_listwidget.clear()

        if self._repos_info is None:
            return

        # For now, we simply show the nodes in sorted order, without respect to dag order
        all_uuids = sorted(
            self._repos_info[dataset_uuid]["DAG"]["Nodes"].keys())
        for node_uuid in all_uuids:
            node_item = QListWidgetItem(node_uuid,
                                        parent=self._node_listwidget)
            node_item.setData(Qt.UserRole, node_uuid)
        self._current_dset = dataset_uuid

        # Select the last one by default.
        last_row = self._node_listwidget.count() - 1
        last_item = self._node_listwidget.item(last_row)
        self._node_listwidget.setCurrentItem(last_item)
        self._update_display()

    def _get_selected_node(self):
        selected_items = self._node_listwidget.selectedItems()
        if not selected_items:
            return None
        selected_node_item = selected_items[0]
        node_item_data = selected_node_item.data(Qt.UserRole)
        return str(node_item_data.toString())

    def _get_selected_data(self):
        selected_items = self._data_treewidget.selectedItems()
        if not selected_items:
            return None, None
        selected_data_item = selected_items[0]
        data_item_data = selected_data_item.data(0, Qt.UserRole).toPyObject()
        if selected_data_item:
            dset_uuid, data_name = data_item_data
        else:
            dset_uuid = data_name = None
        return dset_uuid, data_name

    def _update_display(self):
        """
        Update the path label to reflect the user's currently selected uuid and new volume name.
        """
        hostname, dset_uuid, dataname, node_uuid = self.get_selection()
        full_path = "http://{hostname}/api/node/{uuid}/{dataname}"\
                    "".format( hostname=self._hostname, uuid=node_uuid, dataname=dataname )
        self._full_url_label.setText(full_path)

        ok_button = self._buttonbox.button(QDialogButtonBox.Ok)
        ok_button.setEnabled(dataname != "")
Esempio n. 4
0
class ContentsBrowser(QDialog):
    """
    Displays the contents of a DVID server, listing all repos and the volumes/nodes within each repo.
    The user's selected repo, volume, and node can be accessed via the `get_selection()` method.
    
    If the dialog is constructed with mode='specify_new', then the user is asked to provide a new data name, 
    and choose the repo and node to which it will belong. 
    
    **TODO:**

    * Show more details in node list (e.g. date modified, parents, children)
    * Gray-out nodes that aren't "open" for adding new volumes
    """
    def __init__(self, suggested_hostnames, default_nodes=None, mode='select_existing', selectable_type=VOXEL_TYPENAMES, parent=None):
        """
        Constructor.
        
        suggested_hostnames: A list of hostnames to suggest to the user, e.g. ["localhost:8000"]
        default_nodes: A dict of {hostname : uuid} specifying which node to auto-select for each possible host.
        mode: Either 'select_existing' or 'specify_new'
        parent: The parent widget.
        """
        assert mode in ('select_existing', 'specify_new'), "Invalid mode: {}".format(mode)
        super( ContentsBrowser, self ).__init__(parent)
        self._suggested_hostnames = suggested_hostnames
        self._default_nodes = default_nodes
        self._mode = mode
        self._current_repo = None
        self._repos_info = None
        self._hostname = None
        
        if isinstance(selectable_type, collections.Iterable): 
            self._selectable_types = selectable_type
        else:
            assert isinstance(selectable_type, str), "selectable_type must be str or list-of-str"
            self._selectable_types = (selectable_type,)

        # Create the UI
        self._init_layout()

    VolumeSelection = collections.namedtuple( "VolumeSelection", "hostname repo_uuid data_name node_uuid typename" )
    def get_selection(self):
        """
        Get the user's current (or final) selection.
        Returns a VolumeSelection tuple.
        """
        # Convert to str (Qt tends to convert these into unicode)
        node_uuid = str(self._get_selected_node())
        repo_uuid, data_name, typename = map(str, self._get_selected_data())
        
        if self._mode == "specify_new":
            data_name = str( self._new_data_edit.text() )
        
        return ContentsBrowser.VolumeSelection(str(self._hostname), repo_uuid, data_name, node_uuid, typename)

    def _init_layout(self):
        """
        Create the GUI widgets (but leave them empty).
        """
        hostname_combobox = QComboBox(parent=self)
        self._hostname_combobox = hostname_combobox
        hostname_combobox.setEditable(True)
        hostname_combobox.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Maximum )
        for hostname in self._suggested_hostnames:
            hostname_combobox.addItem( hostname )

        # EventFilter is installed after everything else is initialized. (See below.)
        #hostname_combobox.installEventFilter(self)

        self._connect_button = QPushButton("Connect", parent=self, clicked=self._handle_new_hostname)

        hostname_layout = QHBoxLayout()
        hostname_layout.addWidget( hostname_combobox )
        hostname_layout.addWidget( self._connect_button )

        hostinfo_table = QTableWidget()
        hostinfo_table.setColumnCount(len(SERVER_INFO_FIELDS))
        hostinfo_table.setHorizontalHeaderLabels(SERVER_INFO_FIELDS)
        hostinfo_table.horizontalHeader().setVisible(True)
        hostinfo_table.verticalHeader().setVisible(False)
        hostinfo_table.setRowCount(1)
        hostinfo_table.setItem(0,0, QTableWidgetItem("Placeholder"))
        hostinfo_table.setVisible(False)
        hostinfo_table.resizeRowsToContents()
        hostinfo_table.horizontalHeader().setStretchLastSection(True)
        table_height = hostinfo_table.verticalHeader().sectionSize(0) + hostinfo_table.rowHeight(0)
        hostinfo_table.resize( QSize( hostinfo_table.width(), table_height ) )
        hostinfo_table.setMaximumSize( QSize( 1000, table_height ) )
        hostinfo_table.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)

        host_layout = QVBoxLayout()
        host_layout.addLayout(hostname_layout)
        host_layout.addWidget(hostinfo_table)

        host_groupbox = QGroupBox("DVID Host", parent=self)
        host_groupbox.setLayout( host_layout )
        host_groupbox.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred )
        
        repo_treewidget = QTreeWidget(parent=self)
        repo_treewidget.setHeaderLabels( TREEVIEW_COLUMNS ) # TODO: Add type, shape, axes, etc.
        repo_treewidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred )
        repo_treewidget.itemSelectionChanged.connect( self._handle_data_selection )

        data_layout = QVBoxLayout()
        data_layout.addWidget( repo_treewidget )
        data_groupbox = QGroupBox("Data Volumes", parent=self)
        data_groupbox.setLayout( data_layout )
        
        node_listwidget = QListWidget(parent=self)
        node_listwidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred )
        node_listwidget.itemSelectionChanged.connect( self._update_status )

        node_layout = QVBoxLayout()
        node_layout.addWidget( node_listwidget )
        node_groupbox = QGroupBox("Nodes", parent=self)
        node_groupbox.setLayout( node_layout )

        new_data_edit = QLineEdit(parent=self)
        new_data_edit.textEdited.connect( self._update_status )
        full_url_label = QLabel(parent=self)
        full_url_label.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum )
        text_flags = full_url_label.textInteractionFlags()
        full_url_label.setTextInteractionFlags( text_flags | Qt.TextSelectableByMouse )

        new_data_layout = QVBoxLayout()
        new_data_layout.addWidget( new_data_edit )
        new_data_groupbox = QGroupBox("New Data Volume", parent=self)
        new_data_groupbox.setLayout( new_data_layout )
        new_data_groupbox.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum )

        buttonbox = QDialogButtonBox( Qt.Horizontal, parent=self )
        buttonbox.setStandardButtons( QDialogButtonBox.Ok | QDialogButtonBox.Cancel )
        buttonbox.accepted.connect( self.accept )
        buttonbox.rejected.connect( self.reject )
        buttonbox.button(QDialogButtonBox.Ok).setEnabled(False)

        layout = QVBoxLayout()
        layout.addWidget( host_groupbox )
        layout.addWidget( data_groupbox )
        layout.addWidget( node_groupbox )
        if self._mode == "specify_new":
            layout.addWidget( new_data_groupbox )
        else:
            new_data_groupbox.hide()
        layout.addWidget( full_url_label )
        layout.addWidget( buttonbox )

        # Stretch factors
        layout.setStretchFactor(data_groupbox, 3)
        layout.setStretchFactor(node_groupbox, 1)
        
        self.setLayout(layout)
        self.setWindowTitle( "Select DVID Volume" )
        self.resize(1000, 1000)

        # Initially disabled
        data_groupbox.setEnabled(False)
        node_groupbox.setEnabled(False)
        new_data_groupbox.setEnabled(False)
        
        # Set tab order
        self.setTabOrder(hostname_combobox, repo_treewidget)
        self.setTabOrder(repo_treewidget, node_listwidget)
        self.setTabOrder(node_listwidget, buttonbox)

        # Save instance members
        self._hostinfo_table = hostinfo_table
        self._data_groupbox = data_groupbox
        self._node_groupbox = node_groupbox
        self._new_data_groupbox = new_data_groupbox
        self._repo_treewidget = repo_treewidget
        self._node_listwidget = node_listwidget
        self._new_data_edit = new_data_edit
        self._full_url_label = full_url_label
        self._buttonbox = buttonbox

        # Finally install eventfilter (after everything is initialized)
        hostname_combobox.installEventFilter(self)

    def sizeHint(self):
        return QSize(1000,1000)
    
    def eventFilter(self, watched, event):
        """
        When the user presses the 'Enter' key, auto-click 'Connect'.
        If we've already connected, auto-click 'OK'.
        """
        if watched == self._hostname_combobox \
        and event.type() == QEvent.KeyPress \
        and ( event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter):
            if self._hostname:
                self._buttonbox.button(QDialogButtonBox.Ok).click()
            else:
                self._connect_button.click()
            return True
        return False

    def showEvent(self, event):
        """
        Raise the window when it is shown.
        For some reason, that doesn't happen automatically if this widget is also the main window.
        """
        super(ContentsBrowser, self).showEvent(event)
        self.raise_()

    def _handle_new_hostname(self):
        """
        Called by 'Connect' button.
        Connect to the server, download the server info and repo info,
        and populate the GUI widgets with the data.
        """
        new_hostname = str( self._hostname_combobox.currentText() )
        if '://' in new_hostname:
            new_hostname = new_hostname.split('://')[1] 

        error_msg = None
        self._server_info = None
        self._repos_info = None
        self._current_repo = None
        self._hostname = None

        try:
            # Query the server
            connection = DVIDConnection(new_hostname)
            self._server_info = ContentsBrowser._get_server_info(connection)
            self._repos_info = ContentsBrowser._get_repos_info(connection)
            self._hostname = new_hostname
            self._connection = connection
        except DVIDException as ex:
            error_msg = "libdvid.DVIDException: {}".format( ex.message )
        except ErrMsg as ex:
            error_msg = "libdvid.ErrMsg: {}".format( ex.message )

        if error_msg:
            QMessageBox.critical(self, "Connection Error", error_msg)
            self._populate_node_list(None)
        else:
            self._connect_button.setEnabled(False)
            self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(True)

        enable_contents = self._repos_info is not None
        self._data_groupbox.setEnabled(enable_contents)
        self._node_groupbox.setEnabled(enable_contents)
        self._new_data_groupbox.setEnabled(enable_contents)

        self._populate_hostinfo_table()
        self._populate_repo_tree()
        
    @classmethod
    def _get_server_info(cls, connection):
        status, body, error_message = connection.make_request( "/server/info", ConnectionMethod.GET);
        assert status == httplib.OK, "Request for /server/info returned status {}".format( status )
        assert error_message == ""
        server_info = json.loads(body)
        return server_info

    @classmethod
    def _get_repos_info(cls, connection):
        status, body, error_message = connection.make_request( "/repos/info", ConnectionMethod.GET)
        assert status == httplib.OK, "Request for /repos/info returned status {}".format( status )
        assert error_message == ""
        repos_info = json.loads(body)
        
        # Discard uuids with 'null' content (I don't know why they sometimes exist...)
        repos_info = filter( lambda (uuid, repo_info): repo_info, repos_info.items() )
        return collections.OrderedDict(sorted(repos_info))
    
    def _populate_hostinfo_table(self):
        self._hostinfo_table.setVisible( self._server_info is not None )
        if not self._server_info:
            return

        for column_index, fieldname in enumerate(SERVER_INFO_FIELDS):
            try:
                field = self._server_info[fieldname]
            except KeyError:
                field = "<UNDEFINED>"
            item = QTableWidgetItem(field)
            flags = item.flags()
            flags &= ~Qt.ItemIsSelectable
            flags &= ~Qt.ItemIsEditable
            item.setFlags( flags )
            self._hostinfo_table.setItem(0, column_index, item)
        self._hostinfo_table.resizeColumnsToContents()
        self._hostinfo_table.horizontalHeader().setStretchLastSection(True) # Force refresh of last column.

    def _populate_repo_tree(self):
        """
        Initialize the tree widget of repos and volumes.
        """
        self._repo_treewidget.clear()
        
        if self._repos_info is None:
            return
        
        for repo_uuid, repo_info in sorted(self._repos_info.items()):
            if repo_info is None:
                continue
            repo_column_dict = collections.defaultdict(str)
            repo_column_dict["Alias"] = repo_info["Alias"]
            repo_column_dict["Details"] = "Created: " + repo_info["Created"]
            repo_column_dict["UUID"] = repo_uuid
            repo_column_values = [repo_column_dict[k] for k in TREEVIEW_COLUMNS]
            repo_item = QTreeWidgetItem( self._repo_treewidget, QStringList( repo_column_values ) )
            repo_item.setData( 0, Qt.UserRole, (repo_uuid, "", "") )
            for data_name, data_info in repo_info["DataInstances"].items():
                data_instance_dict = collections.defaultdict(str)
                data_instance_dict["Alias"] = data_name
                typename = data_info["Base"]["TypeName"]
                data_instance_dict["TypeName"] = typename

                is_voxels = (typename in VOXEL_TYPENAMES)
                if is_voxels:
                    start_coord = data_info["Extended"]["MinPoint"]
                    if start_coord:
                        start_coord = tuple(start_coord)
                    stop_coord = data_info["Extended"]["MaxPoint"]
                    if stop_coord:
                        stop_coord = tuple(x+1 for x in stop_coord)
                    if start_coord and stop_coord:
                        shape = tuple(b - a for a,b in zip(start_coord, stop_coord))
                    else:
                        shape = None
                    data_instance_dict["Details"] = "Size={} | Start={} | Stop={}".format( shape, start_coord, stop_coord )

                data_column_values = [data_instance_dict[k] for k in TREEVIEW_COLUMNS]
                data_item = QTreeWidgetItem( repo_item, data_column_values )
                data_item.setData( 0, Qt.UserRole, (repo_uuid, data_name, typename) )

                # If we're in specify_new mode, only the
                # repo parent items are selectable.
                # Also, non-volume items aren't selectable.
                if self._mode == 'specify_new' or typename not in self._selectable_types:
                    flags = data_item.flags()
                    flags &= ~Qt.ItemIsSelectable
                    flags &= ~Qt.ItemIsEnabled
                    data_item.setFlags( flags )
        
        self._repo_treewidget.collapseAll()
        self._repo_treewidget.setSortingEnabled(True)

        if self._hostname in self._default_nodes:
            self._select_node_uuid(self._default_nodes[self._hostname])

        self._repo_treewidget.resizeColumnToContents(0)
        self._repo_treewidget.setFocus()

    def _handle_data_selection(self):
        """
        When the user clicks a new data item, respond by updating the node list.
        """
        selected_items = self._repo_treewidget.selectedItems()
        if not selected_items:
            return None
        item = selected_items[0]
        item_data = item.data(0, Qt.UserRole).toPyObject()
        if not item_data:
            return
        repo_uuid, data_name, typename = item_data
        if self._current_repo != repo_uuid:
            self._populate_node_list(repo_uuid)
        
        self._update_status()

    def _populate_node_list(self, repo_uuid):
        """
        Replace the contents of the node list widget 
        to show all the nodes for the currently selected repo.
        """
        self._node_listwidget.clear()
        
        if self._repos_info is None or repo_uuid is None:
            return
        
        # For now, we simply show the nodes in sorted order, without respect to dag order
        all_uuids = sorted( self._repos_info[repo_uuid]["DAG"]["Nodes"].keys() )
        for node_uuid in all_uuids:
            node_item = QListWidgetItem( node_uuid, parent=self._node_listwidget )
            node_item.setData( Qt.UserRole, node_uuid )
        self._current_repo = repo_uuid

        # Select the last one by default.
        last_row = self._node_listwidget.count() - 1
        last_item = self._node_listwidget.item( last_row )
        self._node_listwidget.setCurrentItem( last_item )
        self._update_status()

    def _get_selected_node(self):
        """
        Return the currently selected node uuid.
        """
        selected_items = self._node_listwidget.selectedItems()
        if not selected_items:
            return None
        selected_node_item = selected_items[0]
        node_item_data = selected_node_item.data(Qt.UserRole)
        return str( node_item_data.toString() )
        
    def _get_selected_data(self):
        """
        Return the repo, data name, and type of the currently selected data volume (or ROI).
        """
        selected_items = self._repo_treewidget.selectedItems()
        if not selected_items:
            return None, None
        selected_data_item = selected_items[0]
        data_item_data = selected_data_item.data(0, Qt.UserRole).toPyObject()
        if selected_data_item:
            repo_uuid, data_name, typename = data_item_data
        else:
            repo_uuid = data_name = typename = None
        return repo_uuid, data_name, typename
    
    def _select_node_uuid(self, node_uuid):
        """
        Locate the repo that owns this uuid, and select it in the GUI.
        If the uuid can't be found, do nothing.
        """
        def select_repotree_item(repo_uuid):
            for row in range(self._repo_treewidget.topLevelItemCount()):
                repo_item = self._repo_treewidget.topLevelItem(row)
                if repo_uuid == repo_item.data(0, Qt.UserRole).toPyObject()[0]:
                    self._repo_treewidget.setCurrentItem(repo_item)
                    repo_item.setExpanded(True)
                    self._repo_treewidget.scrollTo( self._repo_treewidget.selectedIndexes()[0],
                                                    QTreeWidget.PositionAtCenter )
                    break

        def select_nodelist_item(node_uuid):
            for row in range(self._node_listwidget.count()):
                item = self._node_listwidget.item(row)
                if node_uuid == item.data(Qt.UserRole).toPyObject():
                    self._node_listwidget.setCurrentItem( item )
                    break
        
        for repo_uuid, repo_info in sorted(self._repos_info.items()):
            if node_uuid in repo_info["DAG"]["Nodes"].keys():
                # Select the right repo parent item
                select_repotree_item(repo_uuid)

                # Select the right row in the node list
                # (The node list was automatically updated when the repo selection changed, above.)
                select_nodelist_item(node_uuid)
                break

    def _update_status(self):
        """
        Update the path label to reflect the user's currently selected uuid and new volume name.
        """
        hostname, repo_uuid, dataname, node_uuid, typename = self.get_selection()
        full_path = "http://{hostname}/api/node/{uuid}/{dataname}"\
                    "".format( hostname=self._hostname, uuid=node_uuid, dataname=dataname )
        self._full_url_label.setText( full_path )
        
        ok_button = self._buttonbox.button( QDialogButtonBox.Ok )
        ok_button.setEnabled( dataname != "" )
Esempio n. 5
0
class ExercicesTrigonometrie(Panel_API_graphique):

    titre = u"Trigonométrie" # Donner un titre a chaque module

    def __init__(self, *args, **kw):
        Panel_API_graphique.__init__(self, *args, **kw)
        self.canvas.fixe = True

        self.entrees = QVBoxLayout()
        self.entrees.addSpacing(30)

        self.panneau = QLabel('')
        self.entrees.addWidget(self.panneau)

        self.entrees.addStretch()
        self.felicitations = QLabel('')
        self.entrees.addWidget(self.felicitations)

        self.entrees.addSpacing(30)
        self.btn_niveau = QPushButton(u"Niveau suivant", self)
        self.btn_niveau.clicked.connect(self.niveau_suivant)
        self.entrees.addWidget(self.btn_niveau)
        self.entrees.addSpacing(50)

        self.sizer = QHBoxLayout()
        self.sizer.addWidget(self.canvas, 1)
        self.sizer.addLayout(self.entrees, 0.2)
        self.finaliser(contenu=self.sizer)

        self.reinitialiser()


    def reinitialiser(self):
        if param.debug:
            print(u'Module %s: réinitialisation...' % self.nom)
        self.score = 0
        self.niveau = 0
        self.erreurs = 0
        self.niveau_suivant()


    def niveau_suivant(self, niveau=None):
        # On ferme toutes les feuilles ouvertes (inutile en principe),
        # et on en ouvre une nouvelle.
        self.fermer_feuilles()
        # Paramètres par défaut:
        self.canvas.fenetre = -8, 8, -10, 8
        self.canvas.afficher_axes = True
        self.canvas.quadrillage_defaut()
        self.canvas.ratio = None
        self.canvas.repere = ('O', 'i', 'j')
        self.afficher_barre_outils(False)
        # Ne pas éditer les champs/textes avec [Entrée]
        self.canvas.editeur.actif = False
        # Ne pas éditer les objets par un clic droit
        self.canvas.edition_par_clic_droit = False
        # Et on change de niveau...
        if niveau in (None, False):
            # None ou False (False est renvoyé par Qt via QAbstractBouton.clicked)
            self.niveau += 1
        else:
            self.niveau = niveau
        if param.debug:
            print("== Niveau %s ==" % self.niveau)
        getattr(self, 'niveau%s' % self.niveau)()

        self.btn_niveau.setEnabled(False)
        self.felicitations.setStyleSheet(
            """QLabel {background-color: white; padding: 5px; border-radius: 5px;
            color:white;}""")
        self.update_panneau()

    n = niveau_suivant

    # ------------------------------------------------------
    # Niveaux 1 à 7 : lecture graphique d'équation de droite
    # ------------------------------------------------------

    def cercle_trigo(self):
        u"""Construction d'un cercle trigonométrique.
        """
        obj = self.feuille_actuelle.objets
        obj['O'] = O = Point(0, 0, fixe=True)
        obj['I'] = I = Point(1, 0, fixe=True)
        obj['c'] = c = Cercle(O, I)
        for i in range(1, 8):
            a = i*pi/4
            M = Point(cos(a), sin(a), fixe=True)
        for i in range(1, 12):
            if i != 6:
                a = i*pi/6
                M = Point(cos(a), sin(a), fixe=True)
        self.feuille_actuelle.objets['B'] = B
        # Ne pas afficher l'équation !
        d = Droite(A, B, afficher_info=False)
        self.feuille_actuelle.objets['d'] = d
        if xA == xB:
            reponse = 'x=' + str(xA)
        else:
            reponse = ('y=%s*x+%s' % self.eq_reduite(pointA, pointB))
        xmin, xmax, ymin, ymax = self.canvas.fenetre
        print 'Fenetre::', self.canvas.fenetre, '--', xmin, ymin
        champ = Champ('', xmin, ymin, fond=True, couleur_fond='#ffffb5',
                    prefixe=(ur"Dans le repère $(O;\,\vec\imath,\,\vec\jmath)$, "
                             u"la droite $(AB)$ a pour équation "),
                    alignement_horizontal='left', alignement_vertical='bottom',
                    attendu=reponse)
        print 'xy::', champ.xy
        champ.valider = self.valider_eq
        champ.evt_valider = self.compter_points
        self.feuille_actuelle.objets['champ1'] = champ


    def niveau1(self):
        yA = self.relatif(7)
        if yA > 0:
            yB = yA - randint(1, 3)
        else:
            yB = yA + randint(1, 3)
        self.exercice_lire_equation_AB((0, yA), (1, yB))


    def niveau2(self):
        yA = self.relatif(7)
        if yA > 0:
            yB = yA - choice([1, 3, 5])
        else:
            yB = yA + choice([1, 3, 5])
        self.exercice_lire_equation_AB((0, yA), (-2, yB))


    def niveau3(self):
        while True:
            yA = self.relatif(7)
            yB = self.relatif(7)
            if abs(yB - yA) > 5:
                break
        while True:
            xB = self.relatif(7)
            if abs(xB) > 5:
                break
        self.exercice_lire_equation_AB((0, yA), (xB, yB))


    def niveau4(self):
        u"""Droite horizontale."""
        yA = self.relatif(7)
        while True:
            xA = self.relatif(7)
            xB = self.relatif(7)
            if abs(xB - xA) > 5:
                break
        self.exercice_lire_equation_AB((xA, yA), (xB, yA))


    def niveau5(self):
        u"""Droite verticale."""
        xA = self.relatif(7)
        while True:
            yA = self.relatif(7)
            yB = self.relatif(7)
            if abs(yB - yA) > 5:
                break
        self.exercice_lire_equation_AB((xA, yA), (xA, yB))


    def niveau6(self, n=7):
        """Droite oblique ne coupant pas l'axe des ordonnées sur une
        graduation ; il faut donc calculer (ou deviner) l'ordonnée
        à l'origine."""
        for i in xrange(1000):
            while True:
                xA = self.relatif(n)
                xB = self.relatif(n)
                if abs(xB - xA) > 5:
                    break
            while True:
                yA = self.relatif(7)
                yB = self.relatif(7)
                if abs(yB - yA) > 5:
                    break
            # On calcule l'ordonnée à l'origine (sous forme de fraction sympy).
            a, b = self.eq_reduite((xA, yA), (xB, yB))
            print b
            if b.q not in (1, 2):
                # Le dénominateur de l'ordonnée à l'origine ne doit pas être 1 ou 2.
                break
        self.exercice_lire_equation_AB((xA, yA), (xB, yB))


    def niveau7(self):
        self.canvas.ratio = 4
        self.canvas.quadrillages = (((.25, 1), ':', 0.5, 'k'),)
        self.canvas.fenetre = -4.5, 4.5, -10, 8
        print self.canvas.fenetre
        self.niveau6(n=4)
        self.feuille_actuelle.objets['champ1'].y = -9


    def valider_eq(self, reponse, attendu):
        reponse = traduire_formule(reponse)
        assert attendu.count('=') == 1
        if reponse.count('=') != 1:
            return False
        g, d = attendu.split('=')
        attendu = '%s-(%s)' %(g, d)
        g, d = reponse.split('=')
        reponse = '%s-(%s)' %(g, d)
        reponse = reponse.replace(',', '.')
        quotient = '(%s)/(%s)' % (reponse, attendu)
        try:
            # Les deux équations doivent être « proportionelles ».
            return not simplify(S(quotient)).free_symbols
        except (SympifyError, ValueError):
            pass
        except Exception:
            print_error()
        return False


    def eq_reduite(self, A, B):
        u"""Équation réduite exacte de la droite (AB).

        La droite ne doit pas être verticale."""
        xA, yA = A
        xB, yB = B
        assert xA != xB, "Droite verticale !"
        a = S(yB - yA)/(xB - xA)
        b = yA - a*xA
        return a, b


    # --------------------------------------------
    # Niveau 8 : Résoudre graphiquement un système
    # --------------------------------------------

    def niveau8(self):
        u"""Résolution graphique de système.

        Construire deux droites d'équations données.
        Lire les coordonnées du point d'intersection."""
        self.canvas.fenetre = -8, 8, -10, 11
        self.canvas.afficher_axes = True
        self.canvas.quadrillage_defaut()
        self.afficher_barre_outils(True)
        self.canvas.grille_aimantee = True
        ##self.canvas.editeur.actif = True


        # Point d'intersection
        C = self.couple()
        # On génère deux points A, et B, appartenant respectivement
        # aux droites d1 et d2.
        # Contraintes :
        # - les points A, B, C ne doivent pas être alignés ;
        # - les droites (AC) et (BC) ne doivent être ni horizontales,
        #   ni verticales.
        while True:
            A = self.couple()
            B = self.couple()
            # A, B, C non alignés
            if det(vect(A, C), vect(B, C)) == 0:
                continue
            # (AC) et (BC) ni verticales, ni horizontales
            if all((A[0] - C[0], A[1] - C[1], B[0] - C[0], B[1] - C[1])):
                break
        if param.debug:
            print 'A,B,C:', A, B, C
        # on génère les deux équations de droite
        x = S('x')
        ##def eq_latex(pt1, pt2):
            ##a, b = self.eq_reduite(pt1, pt2)
            ##a = str(a)
            ##if a in ('1', '-1'):
                ##a = a[:-1]
            ##b = str(b)
            ##return convertir_en_latex('y=%s*x+%s' % (a, b))
        def format(eq):
            eq = eq.replace('=1*x', '=x').replace('=-1*x', '=-x').replace('+0', '')
            eq = convertir_en_latex(eq)
            return eq
        a, b = self.eq_reduite(A, C)
        eq1 = format('y=%s*x+%s' % (a, b))
        c, d = self.eq_reduite(B, C)
        eq2 = format('y=%s*x+%s' % (c, d))

        xmin, xmax, ymin, ymax = self.canvas.fenetre

        txt = Texte((u"On note $d_1$ la droite d'équation %s, "
                  u"et $d_2$ la droite d'équation %s.\n"
                  u"Construire les droites $d_1$ puis $d_2$ dans le repère ci-dessous.")
                  % (eq1, eq2), xmin, ymax, fond=True, couleur_fond='#ffffb5', fixe=True,
                  alignement_horizontal='left', alignement_vertical='top')
        self.feuille_actuelle.objets['txt1'] = txt
        champ = Champ('', xmin, ymin, prefixe=u"Le couple solution du système est (",
                 alignement_vertical='bottom', alignement_horizontal='left',
                 attendu=str(C), fond=True, couleur_fond='#ffffb5', suffixe=')',
                 )
        self.feuille_actuelle.objets['champ1'] = champ
        champ.valider = self.valider_couple
        champ.evt_valider = self.compter_points
        ch1 = Champ('', visible=False, attendu='ok')
        self.feuille_actuelle.objets['champ_cache_d1'] = ch1
        ch1.evt_valider = self.compter_points
        ch2 = Champ('', visible=False, attendu='ok')
        ch2.evt_valider = self.compter_points
        self.feuille_actuelle.objets['champ_cache_d2'] = ch2
        self.feuille_actuelle.lier(partial(self.verifier_feuille, eq1=(a, b), eq2=(c, d)))


    def verifier_feuille(self, eq1, eq2):
        print eq1, eq2
        for nom, eq in (('d1', eq1), ('d2', eq2)):
            if nom in self.feuille_actuelle.objets.noms:
                d = self.feuille_actuelle.objets[nom]
                d.label(mode='nom')
                champ = self.feuille_actuelle.objets['champ_cache_' + nom]
                M, N = d
                M = (int(M.x), int(M.y))
                N = (int(N.x), int(N.y))
                if self.eq_reduite(M, N) == eq:
                    d.style(couleur='g')
                    champ.texte = 'ok'
                    msg = 'La droite %s est correcte.' % nom
                    if nom == 'd1':
                        msg += ' Construisez maintenant d2.'
                else:
                    print self.eq_reduite(*d), eq
                    d.style(couleur='r')
                    # On peut mettre n'importe quoi différent de ok dans
                    # champ, l'idée étant que si la droite est fausse mais
                    # n'a pas changé, on ne perde pas de point, et par
                    # contre on perde des points en cas de changement si
                    # c'est toujours faux.
                    champ.texte = str(d.equation)
                    msg = "Attention, la droite %s est fausse." % nom
                QToolTip.showText(QCursor.pos(), msg)
                self.canvas.message(msg, temporaire=False)


    def valider_couple(self, reponse, attendu):
        reponse = reponse.replace(';', ',')
        try:
            if S(reponse) == S(attendu):
                return True
        except Exception:
            print_error()
        return False


    def ax_b(self):
        u"Générer une expression sympy de la forme ax+b, avec a, b dans Z."
        return self.relatif()*S('x') + self.relatif()


    def update_panneau(self):
        self.panneau.setStyleSheet(
            """QLabel { padding: 10px; border-width: 2px; border-style:solid;
            border-radius: 5px; border-color:%s; background-color: %s }"""
            %(QColor(30, 144, 255).name(), QColor(176, 226, 255).name())
                        )
        self.panneau.setText((u"<p><b><i>Niveau :</i> %s</b></p>" % self.niveau) +
                                 (u"<p><b><i>Points :</i> %s</b></p>" % self.score) +
                                 (u"<p><i>Erreurs :</i> %s</p>" % self.erreurs))
        champs = self.feuille_actuelle.objets.lister(type=Champ)
        if champs and all(obj.correct for obj in champs):
            if hasattr(self, 'niveau' + str(self.niveau + 1)):
                self.btn_niveau.setEnabled(True)
                self.btn_niveau.setFocus(True)
                self.felicitations.setText(u'<p><b>Félicitations !</b></p>' +
                                           u'<p>Passer au niveau %s</p>' %(self.niveau + 1))
                self.felicitations.setStyleSheet(
                    """QLabel {background-color: %s; padding: 5px;
                       border-radius: 5px;
                       color:white;}""" %QColor(255, 153, 0).name())

            else:
                self.felicitations.setText(u'<p><b>Félicitations !</b></p>' +
                                           u'<p>Dernier niveau terminé !</p>')
                self.felicitations.setStyleSheet(
                    """QLabel {background-color: %s; padding: 5px; border-radius: 5px;
                    color:white;}""" %QColor(102, 205, 0).name())

    ##def _sauvegarder(self, fgeo, feuille = None):
        ##Panel_API_graphique._sauvegarder(self, fgeo, feuille)
        ##fgeo.contenu[u"niveau"] = [str(self.niveau)]
        ##fgeo.contenu[u"expression"] = [self.raw_expression]
        ##fgeo.contenu[u"score"] = [str(self.score)]
        ##fgeo.contenu[u"erreurs"] = [str(self.erreurs)]
##
    def _ouvrir(self, fgeo):
        pass
        ### Il ne doit y avoir qu'une seule feuille ouverte à la fois.
        ### XXX: intégrer cette fonctionnalité directement au Panel.
        ##self.fermer_feuilles()
        ##Panel_API_graphique._ouvrir(self, fgeo)
        ##if fgeo.contenu.has_key(u"expression"):
            ##self.generer_expression(expr=fgeo.contenu[u"expression"][0])
            ##self.dessiner_tableau()
        ##if fgeo.contenu.has_key(u"niveau"):
            ##self.niveau = int(fgeo.contenu[u"niveau"][0])
        ##if fgeo.contenu.has_key(u"score"):
            ##self.score = int(fgeo.contenu[u"score"][0])
        ##if fgeo.contenu.has_key(u"erreurs"):
            ##self.erreurs = int(fgeo.contenu[u"erreurs"][0])
        ##self.update_panneau()

    ##def _affiche(self):
        ##self.dessiner_tableau()


    # --------------------------------
    # Génération de nombres aléatoires
    # --------------------------------

    @staticmethod
    def signe():
        return 2*randint(0, 1) - 1

    def naturel(self, n=15):
        u'''Retourne un entier entre 2 et `n`.'''
        return randint(2, n)

    def relatif(self, n=15):
        u'''Retourne un entier entre -`n` et -2, ou entre 2 et `n`.'''
        # signe: 1 ou -1
        signe = 2*randint(0, 1) - 1
        return self.signe()*self.naturel(n)

    def rationnel(self, n=7):
        u'''Retourne un quotient d'entiers.'''
        while True:
            p = self.naturel(n)
            q = self.naturel(n)
            if p%q:
                break
        return self.signe()*S(p)/S(q)

    def couple(self, m=7, n=7):
        u"""Retourne un couple d'entiers relatifs."""
        return self.relatif(m), self.relatif(n)

    def autocompleter(self):
        ##if self.btn_niveau.isEnabled():
            ##self.niveau_suivant()
        self.btn_niveau.click()
        for t in self.feuille_actuelle.objets.lister(type=Champ):
            t.texte = t.style('attendu', color='g')
        ##self.parent.parent.ligne_commande.setFocus()

    a = property(autocompleter)

    def compter_points(self, **kw):
        if 'correct' in kw and 'correct_old' in kw and 'champ' in kw:
            if kw['correct']:
                if not kw['correct_old']:
                    if not kw['champ'].style('choix'):
                        # C'est plus dur s'il n'y a pas de choix proposé
                        self.score += 1
                    self.score += 1
            else:
                self.score -= 1
                self.erreurs += 1
        if all(obj.correct for obj in self.feuille_actuelle.objets.lister(type=Champ)):
            self.score += 10*(self.niveau + 1)
        self.update_panneau()
Esempio n. 6
0
class ContentsBrowser(QDialog):
    """
    Displays the contents of a DVID server, listing all datasets and the volumes/nodes within each dataset.
    The user's selected dataset, volume, and node can be accessed via the `get_selection()` method.
    
    If the dialog is constructed with mode='specify_new', then the user is asked to provide a new data name, 
    and choose the dataset and node to which it will belong. 
    
    **TODO:**

    * Show more details in dataset list (e.g. shape, axes, pixel type)
    * Show more details in node list (e.g. date modified, parents, children)
    * Gray-out nodes that aren't "open" for adding new volumes
    """
    def __init__(self, suggested_hostnames, mode='select_existing', parent=None):
        """
        Constructor.
        
        suggested_hostnames: A list of hostnames to suggest to the user, e.g. ["localhost:8000"]
        mode: Either 'select_existing' or 'specify_new'
        parent: The parent widget.
        """
        super( ContentsBrowser, self ).__init__(parent)
        self._suggested_hostnames = suggested_hostnames
        self._mode = mode
        self._current_dset = None
        self._repos_info = None
        self._hostname = None
        
        # Create the UI
        self._init_layout()

    VolumeSelection = collections.namedtuple( "VolumeSelection", "hostname dataset_uuid data_name node_uuid" )
    def get_selection(self):
        """
        Get the user's current (or final) selection.
        Returns a VolumeSelection tuple.
        """
        node_uuid = self._get_selected_node()
        dset_uuid, data_name = self._get_selected_data()
        
        if self._mode == "specify_new":
            data_name = str( self._new_data_edit.text() )
        
        return ContentsBrowser.VolumeSelection(self._hostname, dset_uuid, data_name, node_uuid)

    def _init_layout(self):
        """
        Create the GUI widgets (but leave them empty).
        """
        hostname_combobox = QComboBox(parent=self)
        self._hostname_combobox = hostname_combobox
        hostname_combobox.setEditable(True)
        hostname_combobox.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Maximum )
        hostname_combobox.installEventFilter(self)
        for hostname in self._suggested_hostnames:
            hostname_combobox.addItem( hostname )

        self._connect_button = QPushButton("Connect", parent=self, clicked=self._handle_new_hostname)

        hostname_layout = QHBoxLayout()
        hostname_layout.addWidget( hostname_combobox )
        hostname_layout.addWidget( self._connect_button )

        hostname_groupbox = QGroupBox("DVID Host", parent=self)
        hostname_groupbox.setLayout( hostname_layout )
        hostname_groupbox.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum )
        
        data_treewidget = QTreeWidget(parent=self)
        data_treewidget.setHeaderLabels( ["Data"] ) # TODO: Add type, shape, axes, etc.
        data_treewidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred )
        data_treewidget.itemSelectionChanged.connect( self._handle_data_selection )

        data_layout = QVBoxLayout()
        data_layout.addWidget( data_treewidget )
        data_groupbox = QGroupBox("Data Volumes", parent=self)
        data_groupbox.setLayout( data_layout )
        
        node_listwidget = QListWidget(parent=self)
        node_listwidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred )
        node_listwidget.itemSelectionChanged.connect( self._update_display )

        node_layout = QVBoxLayout()
        node_layout.addWidget( node_listwidget )
        node_groupbox = QGroupBox("Nodes", parent=self)
        node_groupbox.setLayout( node_layout )

        new_data_edit = QLineEdit(parent=self)
        new_data_edit.textEdited.connect( self._update_display )
        full_url_label = QLabel(parent=self)
        full_url_label.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum )
        text_flags = full_url_label.textInteractionFlags()
        full_url_label.setTextInteractionFlags( text_flags | Qt.TextSelectableByMouse )

        new_data_layout = QVBoxLayout()
        new_data_layout.addWidget( new_data_edit )
        new_data_groupbox = QGroupBox("New Data Volume", parent=self)
        new_data_groupbox.setLayout( new_data_layout )
        new_data_groupbox.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum )

        buttonbox = QDialogButtonBox( Qt.Horizontal, parent=self )
        buttonbox.setStandardButtons( QDialogButtonBox.Ok | QDialogButtonBox.Cancel )
        buttonbox.accepted.connect( self.accept )
        buttonbox.rejected.connect( self.reject )
        buttonbox.button(QDialogButtonBox.Ok).setEnabled(False)

        layout = QVBoxLayout()
        layout.addWidget( hostname_groupbox )
        layout.addWidget( data_groupbox )
        layout.addWidget( node_groupbox )
        if self._mode == "specify_new":
            layout.addWidget( new_data_groupbox )
        else:
            new_data_groupbox.hide()
        layout.addWidget( full_url_label )
        layout.addWidget( buttonbox )

        # Stretch factors
        layout.setStretchFactor(data_groupbox, 3)
        layout.setStretchFactor(node_groupbox, 1)
        
        self.setLayout(layout)
        self.setWindowTitle( "Select DVID Volume" )

        # Initially disabled
        data_groupbox.setEnabled(False)
        node_groupbox.setEnabled(False)
        new_data_groupbox.setEnabled(False)

        # Save instance members
        self._data_groupbox = data_groupbox
        self._node_groupbox = node_groupbox
        self._new_data_groupbox = new_data_groupbox
        self._data_treewidget = data_treewidget
        self._node_listwidget = node_listwidget
        self._new_data_edit = new_data_edit
        self._full_url_label = full_url_label
        self._buttonbox = buttonbox

    def sizeHint(self):
        return QSize(700, 500)
    
    def eventFilter(self, watched, event):
        if watched == self._hostname_combobox \
        and event.type() == QEvent.KeyPress \
        and ( event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter):
            self._connect_button.click()
            return True
        return False

    def showEvent(self, event):
        """
        Raise the window when it is shown.
        For some reason, that doesn't happen automatically if this widget is also the main window.
        """
        super(ContentsBrowser, self).showEvent(event)
        self.raise_()

    def _handle_new_hostname(self):
        new_hostname = str( self._hostname_combobox.currentText() )
        if '://' in new_hostname:
            new_hostname = new_hostname.split('://')[1] 

        error_msg = None
        self._repos_info = None
        self._current_dset = None
        self._hostname = None
        try:
            # Query the server
            connection = httplib.HTTPConnection( new_hostname )
            self._repos_info = pydvid.general.get_repos_info( connection )
            self._hostname = new_hostname
            self._connection = connection
        except socket.error as ex:
            error_msg = "Socket Error: {} (Error {})".format( ex.args[1], ex.args[0] )
        except httplib.HTTPException as ex:
            error_msg = "HTTP Error: {}".format( ex.args[0] )

        if error_msg:
            QMessageBox.critical(self, "Connection Error", error_msg)
            self._populate_datasets_tree()
            self._populate_node_list(-1)
        else:
            self._connect_button.setEnabled(False)
            self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(True)

        enable_contents = self._repos_info is not None
        self._data_groupbox.setEnabled(enable_contents)
        self._node_groupbox.setEnabled(enable_contents)
        self._new_data_groupbox.setEnabled(enable_contents)

        self._populate_datasets_tree()

    def _populate_datasets_tree(self):
        """
        Initialize the tree widget of datasets and volumes.
        """
        self._data_treewidget.clear()
        
        if self._repos_info is None:
            return
        
        for dset_uuid, dset_info in sorted(self._repos_info.items()):
            dset_item = QTreeWidgetItem( self._data_treewidget, QStringList( dset_uuid ) )
            dset_item.setData( 0, Qt.UserRole, (dset_uuid, "") )
            for data_name in dset_info["DataInstances"].keys():
                data_item = QTreeWidgetItem( dset_item, QStringList( data_name ) )
                data_item.setData( 0, Qt.UserRole, (dset_uuid, data_name) )
                if self._mode == 'specify_new':
                    # If we're in specify_new mode, only the dataset parent items are selectable.
                    flags = data_item.flags()
                    flags &= ~Qt.ItemIsSelectable
                    flags &= ~Qt.ItemIsEnabled
                    data_item.setFlags( flags )
        
        # Expand everything
        self._data_treewidget.expandAll()
        
        # Select the first item by default.
        if self._mode == "select_existing":
            first_item = self._data_treewidget.topLevelItem(0).child(0)
        else:
            first_item = self._data_treewidget.topLevelItem(0)            
        self._data_treewidget.setCurrentItem( first_item, 0 )

    def _handle_data_selection(self):
        """
        When the user clicks a new data item, respond by updating the node list.
        """
        selected_items = self._data_treewidget.selectedItems()
        if not selected_items:
            return None
        item = selected_items[0]
        item_data = item.data(0, Qt.UserRole).toPyObject()
        if not item_data:
            return
        dset_uuid, data_name = item_data
        if self._current_dset != dset_uuid:
            self._populate_node_list(dset_uuid)
        
        self._update_display()

    def _populate_node_list(self, dataset_uuid):
        """
        Replace the contents of the node list widget 
        to show all the nodes for the currently selected dataset.
        """
        self._node_listwidget.clear()
        
        if self._repos_info is None:
            return
        
        # For now, we simply show the nodes in sorted order, without respect to dag order
        all_uuids = sorted( self._repos_info[dataset_uuid]["DAG"]["Nodes"].keys() )
        for node_uuid in all_uuids:
            node_item = QListWidgetItem( node_uuid, parent=self._node_listwidget )
            node_item.setData( Qt.UserRole, node_uuid )
        self._current_dset = dataset_uuid

        # Select the last one by default.
        last_row = self._node_listwidget.count() - 1
        last_item = self._node_listwidget.item( last_row )
        self._node_listwidget.setCurrentItem( last_item )
        self._update_display()

    def _get_selected_node(self):
        selected_items = self._node_listwidget.selectedItems()
        if not selected_items:
            return None
        selected_node_item = selected_items[0]
        node_item_data = selected_node_item.data(Qt.UserRole)
        return str( node_item_data.toString() )
        
    def _get_selected_data(self):
        selected_items = self._data_treewidget.selectedItems()
        if not selected_items:
            return None, None
        selected_data_item = selected_items[0]
        data_item_data = selected_data_item.data(0, Qt.UserRole).toPyObject()
        if selected_data_item:
            dset_uuid, data_name = data_item_data
        else:
            dset_uuid = data_name = None
        return dset_uuid, data_name
    
    def _update_display(self):
        """
        Update the path label to reflect the user's currently selected uuid and new volume name.
        """
        hostname, dset_uuid, dataname, node_uuid = self.get_selection()
        full_path = "http://{hostname}/api/node/{uuid}/{dataname}"\
                    "".format( hostname=self._hostname, uuid=node_uuid, dataname=dataname )
        self._full_url_label.setText( full_path )
        
        ok_button = self._buttonbox.button( QDialogButtonBox.Ok )
        ok_button.setEnabled( dataname != "" )
Esempio n. 7
0
class NotesMainWindow(QMainWindow):
    """The view class"""
    def __init__(self, parent=None):
        super().__init__(parent)

        # Load saved application settings
        self.recentNotes = []

        self.numberOfTabs = 0  # keep track of the total number of tabs in the window
        self.currentPage = None
        self.__mediator = MITM.Mediator.Mediator(
        )  # the mediator object. intermediary btwn view and model

        # create a timer
        timer = QTimer(self)
        # dialogs: saved to avoid eating memory by constant cre- and re- creation
        self.findDialog = None
        self.renameDialog = None
        self.helpAboutDialog = HelpAboutDialog(self)
        self.helpHelpDialog = HelpHelpDialog(self)
        self.recentFilesListWidget = QListWidget

        # create menus first
        self.fileMenu = self.menuBar().addMenu("File")
        self.recentNotesMenu = self.fileMenu.addMenu("Open Recent")
        self.editMenu = self.menuBar().addMenu("Edit")
        self.syncMenu = self.menuBar().addMenu("Sync")
        self.helpMenu = self.menuBar().addMenu("Help")

        # region Actions
        # create the menu actions
        # first the file menu actions
        fileNewAct = self.createAction(self,
                                       "&New",
                                       shortcut=QKeySequence.New,
                                       tooltip="Create a new note",
                                       enabled=True,
                                       slot=self.onNewClick)
        fileOpenRecentNotesAct = self.createAction(self,
                                                   "&Open Recent Note",
                                                   tooltip="open recent notes",
                                                   enabled=True,
                                                   slot=self.onOpenRecentNotes)
        fileQuitAct = self.createAction(self,
                                        "&Exit",
                                        shortcut=QKeySequence.Quit,
                                        tooltip="Exit the application",
                                        enabled=True,
                                        slot=self.close)

        # recent files menu actions
        self.recentFilesMenuAct = self.createAction(self.recentNotesMenu,
                                                    "File 1")

        # edit menu actions
        editInsertAct = self.createAction(self,
                                          "Ins&ert...",
                                          shortcut="Ctrl+I",
                                          tooltip="Insert a media file",
                                          enabled=True)
        editPreferencesAct = self.createAction(
            self,
            "&Preferences",
            shortcut="Ctrl+Shift+P",
            tooltip="Set application preferences",
            enabled=True)
        editFindAct = self.createAction(self,
                                        "&Find",
                                        shortcut=QKeySequence.Find,
                                        tooltip="Find a text string",
                                        slot=self.onFind)
        editUndoAct = self.createAction(
            self,
            "&Undo",
            shortcut=QKeySequence.Undo,
            tooltip="Roll back changes to document",
            slot=self.onUndo)
        editRedoAct = self.createAction(self,
                                        "&Redo",
                                        shortcut=QKeySequence.Redo,
                                        tooltip="Repeat the last action",
                                        slot=self.onRedo)
        editRenameAct = self.createAction(self,
                                          "&Rename...",
                                          shortcut="Ctrl+Shift+R",
                                          tooltip="Rename current note",
                                          slot=self.onRenameNote)

        # sync menu actions
        synCloudAcctAct = self.createAction(self,
                                            "Cloud &Account",
                                            shortcut="Alt+A",
                                            tooltip="Sync with cloud accounts",
                                            enabled=True)

        # help menu actions
        helpHelpAct = self.createAction(self,
                                        "Help",
                                        shortcut=QKeySequence.HelpContents,
                                        tooltip="Display help",
                                        enabled=True,
                                        slot=self.onHelpHelp)
        helpAboutAct = self.createAction(self,
                                         "A&bout",
                                         shortcut="Ctrl+Shift+B",
                                         tooltip="About application",
                                         enabled=True,
                                         slot=self.onHelpAbout)

        boldTextAction = self.createAction(self,
                                           "B",
                                           shortcut="Ctrl+B",
                                           tooltip="Bold text")
        italicsTextAction = self.createAction(self,
                                              "<i>I</i>",
                                              shortcut="Ctrl+I",
                                              tooltip="Italics text")
        # endregion

        # add actions to corresponding menu
        self.addActions_(
            self.fileMenu,
            (fileNewAct, fileOpenRecentNotesAct, fileQuitAct))  # to file menu
        self.addActions_(self.editMenu,
                         (editRenameAct, editUndoAct, editRedoAct, editFindAct,
                          editInsertAct, editPreferencesAct))  # to edit menu
        self.addActions_(self.helpMenu,
                         (helpHelpAct, helpAboutAct))  # to help menu
        self.addActions_(self.recentNotesMenu, (self.recentFilesMenuAct, ))
        # create tool bar and add relevant actions
        allToolBar = self.addToolBar(
            "All Tools"
        )  # tool bar that contains all tool;not separated into file/edit/*
        self.addActions_(allToolBar,
                         (fileNewAct, synCloudAcctAct, editInsertAct,
                          editFindAct, editUndoAct, editRedoAct))

        fontDetailsToolBar = self.addToolBar("Font Details")
        self.fontTypeComboBox = QFontComboBox(
            fontDetailsToolBar)  # get font list present on the user system
        self.fontSizeSpinBox = QSpinBox(fontDetailsToolBar)  # size of the font
        self.fontSizeSpinBox.setMinimum(7)
        self.fontSizeSpinBox.setValue(12)
        self.textBoldButton = QPushButton("B")
        self.addNewNoteButton = QPushButton("+")
        self.textItalicsButton = QPushButton("I")
        maskLabel = QLabel(
            fontDetailsToolBar)  # TODO: experimental, to be removed
        pixmap = QPixmap("Asset/guit.png")  # TODO: experimental, remove
        maskLabel.setPixmap(pixmap.mask())  # TODO: experimental, remove
        self.addWidgetToToolBar(fontDetailsToolBar,
                                (self.fontTypeComboBox, self.fontSizeSpinBox,
                                 self.textBoldButton, self.textItalicsButton))
        # self.addActions_(fontDetailsToolBar, (boldTextAction, italicsTextAction))

        # create a QTabWidget
        self.tabWidget = QTabWidget(self)
        self.tabWidget.setMovable(True)
        self.tabWidget.setTabsClosable(True)
        self.tabWidget.setTabPosition(QTabWidget.South)
        # self.setTabPosition(Qt.BottomDockWidgetArea, QTabWidget.South)  # TODO: not working yet. Handled
        self.tabWidgetTabBar = self.tabWidget.tabBar()
        self.tabWidget.setCornerWidget(self.addNewNoteButton,
                                       Qt.TopRightCorner)

        # create pages  # TODO:     let an event handler handle this job: HANDLED
        self.onWindowStartOpenNote()
        # self.page1 = View.TabbedPage.TabbedPage(self, Note("First", "/path"))
        # # add the pages to the tab Widget
        # self.tabWidget.addTab(self.page1, "Note 1 ")
        # self.currentPage = self.tabWidget.currentWidget()
        self.setCurrentPage()
        #        print("count() returned: ", self.tabWidget.count())

        # create "+" bar:  TODO: find a better way to create a plus tab: DONE
        # self.plusTabIndex = self.tabWidgetTabBar.insertTab(self.tabWidget.count() + 1,
        #                                         "+")  # this tab bears the "+" sign that indicates 'create new tab'
        # self.plusTabWidget = self.tabWidget.widget(self.plusTabIndex)  # get a reference to the plus tab widget

        # create Dock widget that holds find dialog
        # self.dockWidget = QDockWidget(self)
        # self.dockWidget.setAllowedAreas(Qt.TopDockWidgetArea)
        # self.dockWidget.setFeatures(QDockWidget.DockWidgetClosable|QDockWidget.DockWidgetMovable)
        # self.dockWidget.setWidget(FindDialog.FindDialog(self.currentPage, self))
        # self.dockWidget.hide()

        # do window namings and other stuffs
        self.statusbar = QStatusBar(self)
        self.statusbar.setSizeGripEnabled(False)
        self.setStatusBar(self.statusbar)
        self.setCentralWidget(self.tabWidget)
        self.setWindowTitle("Notes")

        # region Signal And Slot Bindings
        self.tabWidget.currentChanged.connect(self.setCurrentPage)
        # self.currentPage.firstTime.connect(self.openPageOnFirstNavigation)
        self.tabWidget.currentChanged.connect(
            self.notifyMediatorOfCurrentPageChange)
        # fileNewAct.triggered.connect(self.onNewClick)
        # fileQuitAct.triggered.connect(self.close)
        self.tabWidget.tabCloseRequested.connect(
            self.confirmTabCloseAndCloseTab)
        # self.tabWidget.tabCloseRequested.connect(self.holla)
        self.fontTypeComboBox.currentFontChanged.connect(self.changeFont)
        self.fontSizeSpinBox.valueChanged.connect(self.changeFontSize)
        self.textBoldButton.clicked.connect(self.toggleTextBold)
        self.textItalicsButton.clicked.connect(self.toggleTextItalics)
        timer.timeout.connect(self.onSaveClick)
        timer.timeout.connect(self.timed)
        self.addNewNoteButton.clicked.connect(self.onNewClick)
        self.currentPage.cursorPositionChanged.connect(
            self.reportCurrentCursor)
        self.recentNotesMenu.aboutToShow.connect(self.updateRecentNotesMenu)
        # editUndoAct.triggered.connect(self.currentPage.undo)
        # editRedoAct.triggered.connect(self.currentPage.redo)
        # endregion

        self.readSettings()
        # QTimer.singleShot(5000, self.onSaveClick)
        # Start the timer
        timer.start(1500000)
        # Application settings

    # region Methods
    def updateRecentNotesMenu(self):
        """Repopulate the recent files menu"""
        self.recentNotesMenu.clear()
        for note in self.recentNotes:
            if os.path.exists(self.mediator.returnNotePath(note)):
                noteName, ext = os.path.basename(note).split(".")
                act = self.createAction(
                    self.recentNotesMenu, noteName, slot=self.loadNotes
                )  #TODO: incomplete update loadNotes() to receive the noteName
                self.addActions_(self.recentNotesMenu, (act, ))

    def onOpenRecentNotes(self):
        """"""

    def reportCurrentCursor(self):
        print("###### cursor position has changed now #####")  # TODO: remove
        print("current font is: ", self.currentPage.currentFont().family())
        self.fontTypeComboBox.setCurrentFont(self.currentPage.currentFont())
        self.fontSizeSpinBox.setValue(
            self.currentPage.currentFont().pointSize())

    def onRecentNotes(self):
        self.recentFilesListWidget.show()

    def toggleTextItalics(self):
        self.currentPage.setFontItalic(not self.currentPage.fontItalic())
        self.currentPage.setFocus()
        # if not self.textItalicsButton.isChecked():
        #     self.textItalicsButton.setChecked(True)
        #     self.currentPage.setFontItalic(True)
        #     self.currentPage.setFocus()
        #     return
        # self.textItalicsButton.setChecked(False)
        # self.currentPage.setFontItalic(False)
        # self.currentPage.setFontItalic(checked)
        # self.currentPage.setFontItalic(not self.currentPage.fontItalic)

    def keyPressEvent(self, event: QKeyEvent):
        if event.modifiers() and Qt.ControlModifier:
            handled = False
        if event.key() == Qt.Key_B:
            self.toggleTextBold()  # bolden text
            handled = True
        elif event.key() == Qt.Key_I:
            self.toggleTextItalics()  # italicise text
            handled = True
            print("Ctrl+I")

        if handled:
            event.accept()
            return
        else:
            QTextEdit.keyPressEvent(self.currentPage, event)

    def toggleTextBold(self):
        self.currentPage.setFontWeight(
            QFont.Normal
            if self.currentPage.fontWeight() > QFont.Normal else QFont.Bold)
        # if not self.textBoldButton.isChecked():
        #     # self.textBoldButton.setChecked(True)
        #     self.currentPage.setFontWeight(QFont.Bold)
        #     self.currentPage.setFocus()
        #     return
        # # self.textBoldButton.setChecked(False)
        # self.currentPage.setFontWeight(QFont.Normal)
        self.currentPage.setFocus()
        # self.currentPage.setFontWeight(QFont.Bold) if checked else \
        #                                     self.currentPage.setFontWeight(QFont.Normal)
        # self.currentPage.setFocus()

    def changeFontSize(self, value: float):
        self.currentPage.setFontPointSize(value)
        self.currentPage.setFocus()

    def changeFont(self, font: QFont):
        self.currentPage.setCurrentFont(font)
        self.currentPage.setFocus()

    def addWidgetToToolBar(self, toolBar: QToolBar, widgets: tuple):
        for widget in widgets:
            toolBar.addWidget(widget)

    def holla(self, index):
        """Debugging Method. To be removed in release code"""
        print("<<<<**** Hey! holla! Removing: .... ", index)

    def timed(self):
        """Debugging Method. To be removed in release code"""
        print("\n$$$$> Called by timer")

    # region Methods

    def onWindowStartOpenNote(self):
        for note_file in self.findNotesInCurrentDirectory(
                applicationDirectory):  # find all the notes in the app dir
            self.createNoteObjectFromNoteFile(
                note_file)  # create note objs from the files
        self.createAndLoadNotes()

    def createNoteObjectFromNoteFile(self, note_file: str):
        self.mediator.createNote(notePath=note_file)

    def createAndLoadNotes(
            self):  # Trent Harmon: When a man loves a woman....song request
        if len(self.mediator.notesCollection) > 0:
            self.loadNotes()
        else:  # couldn't find a note obj
            self.createOnlyOneNote()

    def loadNotes(self):
        """Creates a TabbedPage for each note found in notes collection
            Without reading the contents of the associated note file found
            In note path"""
        note = self.sender().getData() if self.sender() is QAction else None
        if note:
            pageIndex = self.createNotePage(note)
            self.tabWidget.setCurrentIndex(pageIndex)

        for note in self.mediator.NotesCollection:
            self.createNotePage(note)
            # self.mediator.setCurrentlyDisplayedNote(note)

    def createNotePage(self, note):
        page = View.TabbedPage.TabbedPage(self, note)
        pageIndex = self.tabWidget.addTab(page, note.NoteName)
        page.page_number = pageIndex
        return pageIndex

    def createOnlyOneNote(self):
        page1 = View.TabbedPage.TabbedPage(self, Note("First", "/path"))
        # add the pages to the tab Widget
        self.tabWidget.addTab(page1, "Note 1 ")

    def openPageOnFirstNavigation(self):
        """Reads the content of the file on first nav.
            Implements something like lazy loading
            Implementation is whack. 
            Implementation Path:
                onWindowStart -> createNoteObjectFromNoteFile -> createAndLoadNotes ->
                loadNotes -> openPageOnFirstNavigation
            TODO: make a better lazy loading implementatiion"""
        # TODO: make a better lazy loading implementatiion
        print("\n<<<<<I also work\n")
        self.mediator.setCurrentlyDisplayedNote(self.currentPage.getNote())
        try:
            contents = self.mediator.openCurrentNote()
            self.currentPage.setHtml(contents)
            self.currentPage.moveCursor(
                QTextCursor.End, QTextCursor.MoveAnchor
            )  # move the cursor to the end of the document
            self.currentPageIsNowDirty(False)
        except IOError as e:
            logging.error("Note is not on filesystem yet: {}".format(e))

    def currentPageIsNowDirty(self, bool_: bool = False):
        self.currentPage.dirty = False

    @property
    def mediator(self):
        return self.__mediator

    @mediator.setter
    def mediator(self, newMediator):
        if isinstance(newMediator, MITM.Mediator.Mediator):
            self.__mediator = newMediator
        else:
            raise TypeError("newMediator must be of type Mediator")

    def writeSettings(self) -> None:
        settings = QSettings()
        settings.setValue("mainWindow/pos", self.pos())
        settings.setValue("mainWindow/size", self.size())
        settings.setValue("mainWindow/state", self.windowState())
        openedNotes = [note.NotePath for note in self.mediator.NotesCollection]
        settings.setValue("list of opened notes", openedNotes)
        fontAttrList = self.fontTypeComboBox.currentFont().toString()
        print("/////////>>>>  ", fontAttrList)
        settings.setValue("mainWindow/font", fontAttrList)
        settings.setValue("mainWindow/fontSize", self.fontSizeSpinBox.value())

    def readSettings(self):
        settings = QSettings()
        self.resize(settings.value("mainWindow/size", QSize(800, 600)))
        pos = settings.value("mainWindow/pos", QPoint(0, 0))
        listOfOpenedNotes = settings.value("list of opened notes")
        print("Opened notes:", listOfOpenedNotes)
        fontDetails = settings.value("mainWindow/font",
                                     "Architext, -1, -1, False")
        fontSize = settings.value("mainWindow/fontSize", 12)
        family, _, _, fontWeight, italics, *_ = fontDetails.split(",")
        font = QFont(family, int(fontSize), int(fontWeight), bool(italics))
        self.fontTypeComboBox.setCurrentFont(font)
        self.fontSizeSpinBox.setValue(int(fontSize))
        self.textItalicsButton.click() if font.italic(
        ) else self.textItalicsButton.clearFocus(
        )  #TODO: code smell, review later
        self.textBoldButton.click() if font.bold(
        ) else self.textBoldButton.clearFocus(
        )  #TODO: code smell, review later
        self.move(pos)
        self.restoreState(settings.value("mainWindow/state"))

    def setCurrentPage(self):
        """Updates the self.currentPage variable"""
        self.currentPage = self.tabWidget.currentWidget()
        self.currentPage.I_becameCurrentPage()
        print(":::::::::  New current page set: ",
              self.currentPage.getNote().NoteName)  # TODO: remove

    def notifyMediatorOfCurrentPageChange(self):
        self.mediator.setCurrentlyDisplayedNote(self.currentPage.getNote())

    def onNewClick(self):
        """Create a new area to write new note"""
        tabCount = self.tabWidget.count() + 1
        noteObject = self.mediator.createNote(tabCount)
        page = View.TabbedPage.TabbedPage(self, note=noteObject)
        newPageIndex = self.tabWidget.addTab(
            page,
            "Note {}".format(tabCount))  # add the page to the parent tabWidget
        page.page_number = newPageIndex  # assign a number to the page
        self.numberOfTabs = self.tabWidget.count()
        # self.tabWidgetTabBar.moveTab(self.plusTabIndex,
        #             self.numberOfTabs)  # move the "+" tab to the end TODO: update the index of the plus tab widget
        self.moveToTab(newPageIndex)
        self.currentPage.setFocus()

    def moveToTab(self, newPageIndex: int):
        self.tabWidgetTabBar.setCurrentIndex(
            newPageIndex)  # switch to the just created page

    def onSaveClick(self, pages: list = []):
        """This method gets the text from the current page;
                Passes the text along with the attached Note object to the mediator to save
        """
        if len(pages) == 0:  # pages is a list of note pages to be saved
            pages.append(self.currentPage)

        for page in pages:
            if page.dirty:
                rich_text = page.toHtml(
                )  # get the text from the current document
                # note = self.currentPage.getNote()  # get the Note object attached to this page
                try:
                    self.mediator.saveNote(rich_text, page.getNote(
                    ))  # give the text document to the mediator
                    self.currentPageIsNowDirty(False)
                except IOError as error:
                    QMessageBox(self, "File Save Error",
                                "Could not save the file", "Ok")
                    assert isinstance(
                        error, IOError
                    )  # pycharm suggested this, I don't think it's needed here
                    print("Error", "IOError occurred", error)
                    logging.log(1, "IOError occurred", error)
                except NoNoteException as error:
                    # QMessageBox.critical(self, "File Save Error", "Could not save the file: {}".format(error),
                    #             "Ok")  # TODO:Not sure this will work yet. Handled
                    print("Error", "IOError occurred: No Note", error)
                    logging.log(1, error)
            else:
                print("\n-----> No changes made to code")

    def onRenameNote(self):
        """Rename a Note"""
        # TODO : create a rename dialog window later
        renameDialog = RenameDialog(self)
        if renameDialog.exec_():
            newName = renameDialog.getNewName()
            try:
                self.mediator.renameNote(newName)
                self.tabWidget.setTabText(self.currentPage.page_number,
                                          newName)
            except Exception as e:  # TODO: what error exactly
                print("Error renaming this file:   ",
                      e)  # TODO: do better, inform  the user of the problem
                raise e

    def onUndo(self):
        """Connect to QTextEdit undo slot. A work around for inability to connect a QAction to
           QTextEdit undo slot.
        """
        self.currentPage.undo()

    def onRedo(self):
        """Connect to QTextEdit undo slot. A work around for inability to connect a QAction to
            QTextEdit undo slot.
         """
        self.currentPage.redo()
        # cus = CustomSignal()
        # cus.redoTriggered.connect(QTextEdit.redo)

    def onFind(self):  # TODO: song request Paul I.K. Dairo: Mo wa dupe
        textSelected = self.currentPage.getDocumentCursor().selectedText()
        print("Text Selected is:  ", textSelected)
        self.findDialog = FindDialog.FindDialog(self.currentPage, self,
                                                textSelected)
        self.findDialog.show()
        # self.dockWidget.show()

    def onHelpAbout(self):
        self.helpAboutDialog.show()

    def onHelpHelp(self):
        self.helpHelpDialog.show()

    def closeEvent(self, event: QEvent):
        # TODO: raise a dialog that prompts the user to accept the closing of the application
        """
        :type event: QEvent
        """
        numberOfPages = self.tabWidget.count()
        widgetList = [
            self.tabWidget.widget(widget) for widget in range(numberOfPages)
        ]
        # self.onSaveClick(widgetList)  # save notes first
        self.writeSettings()
        event.accept()

    def confirmTabCloseAndCloseTab(self, index):
        pageToRemove = self.tabWidget.widget(index)
        if pageToRemove.dirty and isinstance(pageToRemove,
                                             View.TabbedPage.TabbedPage):
            print("Tab Close requested")
            dialog = QMessageBox.warning(self, "Confirm",
                                         "Do you want to close the tab?", "Ok",
                                         "Cancel", "", 1)
            if dialog != 0:
                return
        self.tabWidget.removeTab(pageToRemove.page_number)
        self.recentNotes.append(pageToRemove.getNote())

    @staticmethod
    def addActions_(parent, actions: tuple) -> None:
        for action in actions:
            parent.addAction(action)

    @staticmethod
    def createAction(parent: QObject,
                     text: str,
                     icon: object = None,
                     shortcut: str = None,
                     tooltip: str = None,
                     enabled: bool = False,
                     signal: str = "triggered",
                     slot: object = None) -> object:
        action = QAction(parent)
        action.setText(text)
        if icon:
            action.setIcon(
                QIcon(icon))  # icon is expected as a string argument
        if shortcut:
            action.setShortcut(shortcut)
        if tooltip:
            action.setToolTip(tooltip)
        if enabled:
            action.setEnabled(enabled)

        if slot and signal == "triggered":
            action.pyqtConfigure(triggered=slot)

        return action

    @staticmethod
    def findNotesInCurrentDirectory(directory: str):
        if os.path.exists(directory):
            for path, dirs, files in os.walk(directory):
                for file in files:
                    if file.endswith(".dn"):
                        fullPath = os.path.join(directory, path, file)
                        yield os.path.normpath(os.path.abspath(fullPath))

    def __str__(self):
        print("I am a note window with id {}".format(self.winId()))

    def __repr__(self):
        print(r"I am a note")