9
votes

NSTextField garde le focus / premier répondeur après NSPopover

Le but de cette application est de s'assurer que l'utilisateur a saisi un certain texte dans un NSTextField. Si ce texte ne figure pas dans le champ, ils ne devraient pas être autorisés à quitter le champ.

Étant donné une application macOS avec un champ de texte de sous-classe, un bouton et un autre champ NSTextField générique. Lorsque vous cliquez sur le bouton, un NSPopover est affiché qui est `` attaché '' au champ qui est contrôlé par un NSViewController appelé myPopoverVC.

Par exemple, l'utilisateur entre 3 dans le champ du haut puis clique sur Afficher Bouton popover qui affiche le popover et fournit un indice: "Qu'est-ce que 1 + 1 est égal".

entrez la description de l'image ici

Notez que ce popover a un champ étiqueté 1er resp alors lorsque le popover apparaît, ce champ devient le premier répondeur. Rien ne sera entré pour le moment - c'est juste pour cette question.

L'utilisateur clique sur le bouton Fermer, qui ferme la fenêtre popover. À ce stade, que devrait-il se passer si l'utilisateur clique ou s'éloigne du champ avec le `` 3 '', l'application ne devrait pas permettre ce mouvement - peut-être en émettant un bip ou un autre message. Mais que se passe-t-il lorsque le popover se ferme et que l'utilisateur appuie sur Tab

entrez la description de l'image ici

Même si ce champ contient le '3' avait une bague de mise au point, qui devrait indiquer à nouveau le premier répondeur dans cette fenêtre, l'utilisateur peut cliquer ou s'éloigner de celle-ci car la fonction textShouldEndEditing n'est pas appelée. Dans ce cas, j'ai cliqué sur le bouton de fermeture dans le popover, le champ '3' avait une bague de mise au point et j'ai appuyé sur l'onglet, qui est ensuite allé au champ suivant.

C'est la fonction dans le texte sous-classé champ qui fonctionne correctement après la saisie du texte dans le champ. Dans ce cas, si l'utilisateur tape un 3 puis appuie sur Tab, le curseur reste dans ce champ.

override func becomeFirstResponder() -> Bool {
    let e = self.currentEditor()
    print(e)
    return super.becomeFirstResponder()
}

Le code du bouton showPopover définit l'indicateur aboutToShowPopover sur true, ce qui permettra la sous-classe pour afficher le popover. (défini sur false lorsque le popover se ferme)

La question est donc de savoir quand le popover se ferme, comment retourner le statut firstResponder au champ de texte d'origine? Il semble avoir le statut de premier répondant, et il pense avoir ce statut bien que textShouldEndEditing ne soit pas appelé. Si vous tapez un autre caractère dans le champ, alors tout fonctionne comme il se doit. C'est comme si l'éditeur de champ de la fenêtre et le champ contenant le '3' étaient déconnectés pour que l'éditeur de champ ne passe pas d'appels vers ce champ.

Le bouton appelle une fonction qui contient ceci: p>

parentVC.dismiss(myPopoverVC)

la fermeture de NSPopover est

    let contentSize = myPopoverVC.view.frame
    theTextField.aboutToShowPopover = true
    parentVC.present(myPopoverVC, asPopoverRelativeTo: contentSize, of: theTextField, preferredEdge: NSRectEdge.maxY, behavior: NSPopover.Behavior.applicationDefined)
    NSApplication.shared.activate(ignoringOtherApps: true)

Une autre information. J'ai ajouté ce morceau de code au contrôle NSTextField sous-classé.

override func textShouldEndEditing(_ textObject: NSText) -> Bool {

    if self.aboutToShowPopover == true {
       return true
    }

    if let editor = self.currentEditor() { //or use the textObject
        let s = editor.string

        if s == "2" {
            return true
        }

        return false
    }

Lorsque le popover se ferme et que textField devient le premier répondeur de Windows, ce code s'exécute mais imprime nil. Ce qui indique que tant qu'il est le premier répondeur, il n'a pas de connexion avec la fenêtre fieldEditor et ne recevra pas d'événements. Pourquoi?

Si quelque chose n'est pas clair, veuillez demander.


12 commentaires

Non pertinent pour le problème, mais dans textShouldEndEditing , utilisez textObject au lieu de self.currentEditor .


Avez-vous essayé d'obtenir l'éditeur de champ pour le champ de texte et de le rendre à nouveau premier répondeur après la fermeture du popover?


@Willeke Merci pour l'entrée - le textObject est évidemment le meilleur choix, alors j'ai mis à jour la question.


@Willeke J'ai donc traversé beaucoup de girations pour essayer de rétablir la connexion des champs à FieldEditor. Utilisation de la fenêtre de contrôle pour en faire le premier répondant ... self.window.makeFirstResponder (self) etc. J'ai mis à jour la question pour répondre à votre question.


Que se passe-t-il si vous définissez d'abord la fenêtre comme son propre premier répondeur ( self.window.makeFirstResponder (nil) ) et que vous la remettez ensuite dans le champ de texte?


@KenThomases Merci - J'y ai pensé. Lors de la suppression du premier répondeur et de sa réaffectation, le NSTextField est à nouveau le premier répondeur, ce champ de texte ne reçoit aucun événement ou message tant que vous n’avez pas saisi quelque chose dans le champ.


Comment le champ de texte termine-t-il l'édition si textShouldEndEditing renvoie false ?


@Willeke Hmmm. Je ne suis pas sûr de comprendre cette question. Lorsque le popover est ignoré et que le focus revient au NSTextField, le champ peut être cliqué ou tabulé car textShouldEndEditing n'est jamais appelé, il n'a donc pas la possibilité de renvoyer false (ou true). Cette fonction n'est appelée que s'il y a une pression manuelle sur une touche dans le champ - alors la fonction est appelée comme vous vous en doutez. Il semble presque que le champ ait le focus mais l'éditeur de fenêtre ne «s'active» pas ou n'a pas le focus jusqu'à ce qu'il y ait un événement key down.


J'ai essayé de reproduire le problème mais le champ de texte ne s'arrête pas à l'édition lorsque j'affiche un popover. Lorsque le popover se ferme, le champ de texte est toujours en cours d'édition et textShouldEndEditing est appelé.


@Willeke Merci d'avoir jeté un coup d'œil. J'ai mis à jour et clarifié la question car il semble que j'ai omis quelques détails. Vous avez raison à 100% dans votre observation - le champ initial maintient la concentration dans le cas que vous présentez. Ce que j'ai oublié, c'est qu'il y a un champ de texte sur le popover, donc quand il s'ouvre, ce champ devient le premier répondeur, puis lorsque le popover ferme le champ d'origine ressemble à nouveau le premier répondeur mais aucun appel lui sont envoyés pour déclencher la fonction textShouldEndEditing afin d'éviter de quitter le champ.


self.currentEditor () retournera l'éditeur de champ après super.becomeFirstResponder () .


@Willeke Hmm. Si vous jetez un œil à la dernière partie de la question, override func devenirFirstResponder () renvoie un booléen, il n'y a donc pas moyen d'appeler self.currentEditor () après le retour dans cette fonction. Ou voulez-vous dire appeler cela dans une autre fonction qui suit devientFirstResponder ? Si tel est le cas, où trouver l'éditeur actuel? L'autre problème est que même si cela renvoie l'éditeur de champ actuel, ce n'est pas l'éditeur de champ windows donc les événements ne sont donc pas passés de l'éditeur de champ de la fenêtre à son délégué, qui devrait être l'éditeur de champs .


3 Réponses :


2
votes

Si vous sous-classez chacun de vos NSTextField, vous pouvez remplacer la méthode devenirFirstResponder et la faire envoyer self à une classe déléguée que vous allez créer, qui conservera une référence de le premier répondant actuel:

Superclasse NSTextField:

class MyViewController: NSViewController, MyFirstResponderDelegate, MyPopUpDismissDelegate {
    var currentFirstResponderTextField: NSTextField?

    func setCurrentResponder(textField: NSTextField) {
        self.currentFirstResponderTextField = textField
    }

    func didDismisssPopUp() {
        guard let isLastTextField = self.currentFirstResponderTextField else  {
            return
        }
        self.isLastTextField?.window?.makeFirstResponder(self.isLastTextField)
    }
}

( myRespondersDelegate : serait éventuellement votre NSViewController)

Remarque : n'utilisez pas la même superclasse pour vos alertes TextFields et ViewController TextFields. Utilisez cette superclasse avec des fonctionnalités supplémentaires uniquement pour les TextFields que vous souhaitez retourner à firstResponder après la fermeture d'une alerte.

Délégué NSTextField:

class MyViewController: NSViewController, MyFirstResponderDelegate {
    var currentFirstResponderTextField: NSTextField?

    func setCurrentResponder(textField: NSTextField) {
        self.currentFirstResponderTextField = textField
    }
}

Maintenant, après votre pop est rejetée, vous pouvez dans viewWillAppear ou créer une fonction de délégué qui sera appelée sur un pop-up ignorer didDismisss (dépend de la façon dont votre pop-up est mis en œuvre, je vais montrer l'option délégué) Vérifiez si un TextField a existé, et refaites-le, le firstResponder

Délégué contextuel:

override func becomeFirstResponder() -> Bool {
        self.myRespondersDelegate.setCurrentResponder(self)
        return super.becomeFirstResponder()
    }


4 commentaires

Merci d'avoir consacré du temps à cette réponse. Malheureusement, cela a le même résultat. Le problème n'est pas que le NSTextField devienne le premier répondeur; lorsque le NSPopver se ferme, un appel à thatTextField.window.makeFirstResponder (thatTextField) accomplit cela. Cependant, le problème est que même s'il a le statut de premier répondeur, il ne reçoit pas d'événements donc les fonctions textField telles que textShouldEndEditing ne sont pas appelées. La seule façon pour ces événements d'être acheminés vers le champ est de taper une clé dans le champ - puis tout fonctionne comme avant le popover.


J'ai clarifié la question davantage si vous voulez y jeter un coup d'œil.


@Jay je pense qu'il me manque quelque chose, car je ne peux pas me reproduire


Il n'y avait probablement pas assez d'informations dans ma question originale à reproduire car j'ai omis qu'il y avait un champ textField sur le Popover qui, lorsque le popover s'ouvre, enlève le statut de premier répondant du champ dans la vue principale. Voir ma question mise à jour avec des captures d'écran - je pense que cela apporte plus de clarté au problème.



3
votes

Voici ma tentative avec l'aide de Comment peut-on commencer par programme une session d'édition de texte dans un NSTextField? et Comment puis-je empêcher mon NSTextField de mettre en surbrillance son texte au démarrage de l'application? :

La plage sélectionnée est enregistrée dans textShouldEndEditing et restaurée dans devenirFirstResponder . insertText (_: replacementRange :) démarre une session d'édition.

var savedSelectedRanges: [NSValue]?

override func becomeFirstResponder() -> Bool {
    if super.becomeFirstResponder() {
        if self.aboutToShowPopover {
            if let ranges = self.savedSelectedRanges {
                if let fieldEditor = self.currentEditor() as? NSTextView {
                    fieldEditor.insertText("", replacementRange: NSRange(location: 0, length:0))
                    fieldEditor.selectedRanges = ranges
                }
            }
        }
        return true
    }
    return false
}

override func textShouldEndEditing(_ textObject: NSText) -> Bool {
    if super.textShouldEndEditing(textObject) {
        if self.aboutToShowPopover {
            let fieldEditor = textObject as! NSTextView
            self.savedSelectedRanges = fieldEditor.selectedRanges
            return true
        }
        let s = textObject.string
        if s == "2" {
            return true
        }
    }
    return false
}

Peut-être renommer aboutToShowPopover .


4 commentaires

Question sur if self.aboutToShowPopover { Je suppose que vous ne vouliez pas le tester pour nil mais le vérifier pour faux comme dans if self.aboutToShowPopover == false {?


Vas-y toi. Cela fonctionne et capture et conserve le focus dans le champ et provoque le déclenchement du textShouldEndEditing après la fermeture du popover. La solution était en fait plus simple car je pense que fieldEditor.insertText ("", replacementRange: NSRange (location: 0, length: 0)) était la clé. Il semble que l'appel «active» l'éditeur de champ Windows et définit ce champ comme son délégué comme il se doit.


Je ne savais pas que votre aboutToShowPopover est facultatif, le mien ne l'est pas.


Ce n'est pas facultatif mais je vois ce que vous dites dans votre code. C'est une variable de classe définie sur false initialement var aboutToShowPopover = false et est définie sur true lorsque l'utilisateur clique sur le bouton pour afficher le popover, ce qui permet ensuite à textShouldEndEditing de renvoyer true donc le popover peut être affiché. Lorsque le popover se ferme, cette variable est définie sur false afin que textShouldEndEditing puisse déterminer si elle contient une valeur valide. J'ai ajouté une réponse avec cette information pour montrer la solution finale. Cela fonctionne très bien au fait, merci encore.



0
votes

Un grand merci à Willeke pour son aide et sa réponse qui ont conduit à une solution assez simple.

Le grand problème ici était que lorsque le popover s'est fermé, le champ «concentré» était le champ d'origine. Cependant, il semble (pour une raison quelconque) que l'éditeur de champ Windows se soit déconnecté de ce champ, de sorte que des fonctions telles que control: textShouldEndEditing n'étaient pas passées au champ sous-classé dans la question.

Exécution de cette ligne lorsque field devient le premier reponder semble reconnecter l'éditeur de champ Windows avec ce champ afin qu'il reçoive les messages des délégués

override func textShouldEndEditing(_ textObject: NSText) -> Bool {

    if self.aboutToShowPopover == true {
        return true
    }

    let s = textObject.string

    if s == "2" {
        return true
    }

    return false
}

override func becomeFirstResponder() -> Bool {

    if super.becomeFirstResponder() == true {
        if let myEditor = self.currentEditor() as? NSTextView {
            let range = NSMakeRange(0, 0)
            myEditor.insertText("", replacementRange: range)
        }
        return true
    }

    return false
}

La solution finale était donc une combinaison des deux fonctions suivantes .

fieldEditor.insertText("", replacementRange: range)


0 commentaires