GTK+ par l'exemple


précédentsommairesuivant

XVI. Ouvrir plusieurs fichiers en même temps

XVI-A. Aperçu

Image non disponible
Cliquez pour agrandir

XVI-B. Mise en place des onglets

Actuellement, nous ne pouvons ouvrir qu'un seul fichier à la fois. Les éditeurs de texte avancés proposent généralement la possibilité d'ouvrir plusieurs documents simultanément grâce à un système d'onglets. C'est ce que nous allons mettre en place grâce au widget GtkNotebook.
A la place du GtkTextView, nous créons un GtkNotebook et comme nous allons avoir besoin de modifier de widget (ajout/suppression de pages), nous gardons un pointeur dessus dans notre structure globale :

main.c
Sélectionnez
  /* Creation de la page d'onglets */
  {
    GtkWidget *p_notebook = NULL;

    p_notebook = gtk_notebook_new ();
    gtk_container_add (GTK_CONTAINER (p_main_box), p_notebook);
    /* ... */
    docs.p_notebook = GTK_NOTEBOOK (p_notebook);
  }

Donc à l'ouverture de l'éditeur, aucun onglet est ouvert, ils seront ajoutés lorsque l'utilisateur crée un document ou en ouvre un. Par chance (ou grâce à l'ingéniosité de l'auteur de ce document, je vous laisse choisir :D), l'ajout d'une nouvelle page se fait uniquement dans la fonction cb_new. Nous avons anticipé la mise en place d'onglet au début de ce tutoriel en créant la structure docs_t dont seul le champs actif était utilisé, maintenant, il faut ajouter chaque document ouvert à la GList :

callback.c
Sélectionnez
void cb_new (GtkWidget *p_widget, gpointer user_data)
{
  document_t *nouveau = NULL;

  nouveau = g_malloc (sizeof (*nouveau));
  nouveau->chemin = NULL;
  /* Le document vient d'etre ouvert, il n'est donc pas modifie */
  nouveau->sauve = TRUE;
  docs.tous = g_list_append (docs.tous, nouveau);
  {
    gint index = 0;
    GtkWidget *p_scrolled_window = NULL;

    p_scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (p_scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    nouveau->p_text_view = GTK_TEXT_VIEW (gtk_text_view_new ());
    {
      GtkTextBuffer *p_buffer = NULL;

      p_buffer = gtk_text_view_get_buffer (nouveau->p_text_view);
      g_signal_connect (G_OBJECT (p_buffer), "changed", G_CALLBACK (cb_modifie), NULL);
    }
    gtk_container_add (GTK_CONTAINER (p_scrolled_window), GTK_WIDGET (nouveau->p_text_view));
    index = gtk_notebook_append_page (docs.p_notebook, p_scrolled_window, GTK_WIDGET (gtk_label_new ("Nouveau document")));
    gtk_widget_show_all (p_scrolled_window);
    gtk_notebook_set_current_page (docs.p_notebook, index);
  }

  /* parametres inutilises */
  (void)p_widget;
  (void)user_data;
}

Première chose intéressante, il n'est plus nécessaire de fermer le document pour en ouvrir un autre. Ensuite, plutôt que de modifier le champ actif, on crée un nouveau document que l'on ajoute à la GList. Le GtkTextView est créé comme précédemment mis à part qu'il est ajouté à un nouvel onglet que l'on crée grâce à la fonction :

 
Sélectionnez
gint gtk_notebook_append_page (GtkNotebook *notebook, GtkWidget *child, GtkWidget *tab_label);

Qui a besoin du widget enfant (il s'agit du GtkScrolledWindow contenant le GtkTextView) et d'un second widget qui sera affiché dans l'onglet (nous laissons GTK+ mettre un texte par défaut, nous verrons plus loin comment améliorer cela).
Une fois l'onglet créé, gtk_notebook_append_page nous retourne la position de la nouvelle page créée qui va nous servir à rendre la page active grâce à la fonction :

 
Sélectionnez
void gtk_notebook_set_current_page (GtkNotebook *notebook, gint page_num);

XVI-C. Changement de page

Pour pouvoir mettre à jour le document actif lorsque l'utilisateur navigue entre les différents onglets, il suffit d'intercepter le signal switch-page :

main.c
Sélectionnez
g_signal_connect (G_OBJECT (p_notebook), "switch-page", G_CALLBACK (cb_page_change), NULL);

La fonction cb_page_change récupére la position de la page courante et fait pointer actif vers la structure document_t correspondante stockée dans la GList :

callback.c
Sélectionnez
void cb_page_change (GtkNotebook *notebook, GtkNotebookPage *page, guint page_num, gpointer user_data)
{
  docs.actif = g_list_nth_data (docs.tous, page_num);

  /* parametres inutilises */
  (void)notebook;
  (void)user_data;
}

Comme GTK+ fait bien les choses, la fonction gtk_notebook_set_current_page que nous appelons lors de la création d'un document émet ce signal, donc pas besoin de recopier ce code dans cb_new.

XVI-D. Fermer un onglet

La dernière fonction qui nécessite quelques modifications, est cb_close, il faut maintenant supprimer le document de la GList, libérer la mémoire et supprimer l'onglet :

callback.c
Sélectionnez
void cb_close (GtkWidget *p_widget, gpointer user_data)
{
  /* Avant de fermer, il faut verifier qu'un document a bien ete ouvert */
  if (docs.actif)
  {
    /* ... */
    {
      docs.tous = g_list_remove (docs.tous, docs.actif);
      g_free (docs.actif->chemin), docs.actif->chemin = NULL;
      g_free (docs.actif), docs.actif = NULL;
      gtk_notebook_remove_page (docs.p_notebook, gtk_notebook_get_current_page (docs.p_notebook));
      if (gtk_notebook_get_n_pages (docs.p_notebook) > 0)
      {
        docs.actif = g_list_nth_data (docs.tous, gtk_notebook_get_current_page (docs.p_notebook));
      }
      else
      {
        docs.actif = NULL;
      }
    }
  }
  else
  {
    print_warning ("Aucun document ouvert");
  }

  /* parametres inutilises */
  (void)p_widget;
  (void)user_data;
}

Il reste plus qu'à supprimer l'appel à la fonction gtk_widget_set_sensitive dans la fonction open_file, maintenant devenu inutile.

Bizarrement la suppression d'un onglet n'émet pas de signal switch-page, nous devons le faire manuellement.

XVI-E. Fermer tous les onglets

Maintenant que nous pouvons ouvrir plusieurs documents en même temps, il est nécessaire de les fermer tous lorsqu'on quitte le programme.
J'ai essayé plusieurs méthodes avant de trouver une méthode convenable. Contrairement à ce que l'on pourrait penser, il ne suffit pas d'utiliser la fonction g_list_foreach, qui permet de parcourir l'ensemble d'une liste chaînée, et de fermer les documents un à un. Le problème vient des documents non sauvegardés, puisque l'utilisateur peut à tout moment décider d'annuler l'enregistrement d'un document ce qui nous oblige à annuler la fermeture de l'application.
Le principe que j'ai choisi d'adopter consiste à boucler tant que tous les documents ne sont pas fermés (dans ce cas docs.actif vaut NULL) et de demander la fermeture du premier onglet (puisque le numéro du dernier change à chaque itération). Si l'utilisateur choisit d'annuler l'enregistrement, dans ce cas l'onglet n'est pas fermé et le nombre de documents ouverts est le même avant et après l'appel à cb_close, nous mettons alors fin à la boucle et signalons l'annulation en retournant FALSE, si tous les documents ont bien été fermés, la fonction renvoit TRUE :

 
Sélectionnez
static gboolean close_all (void)
{
  gboolean ret = TRUE;

  while (docs.actif)
  {
    gint tmp = gtk_notebook_get_n_pages (docs.p_notebook);

    gtk_notebook_set_current_page (docs.p_notebook, 0);
    cb_close (NULL, NULL);
    if (gtk_notebook_get_n_pages (docs.p_notebook) >= tmp)
    {
      ret = FALSE;
      break;
    }
  }
  return ret;
}

Et la fonction cb_quit devient :

 
Sélectionnez
void cb_quit (GtkWidget *p_widget, gpointer user_data)
{
  if (close_all ())
  {
    g_free (docs.dir_name), docs.dir_name = NULL;
    gtk_main_quit();
  }

  /* parametres inutilises */
  (void)p_widget;
  (void)user_data;
}

XVI-F. Modifier le titre de la page

Pour plus d'esthétisme, nous allons modifier les titres des onglets. Pour les nouveaux documents, nous afficherons un texte par défaut (Nouveau document par exemple), une fois enregistré nous afficherons le nom du fichier (sans le chemin pour faire court). Et lorsque le document a été modifié depuis la dernière sauvegarde, nous ajouterons un petit astérisque à côté du titre.
Les bases étant posées, nous allons commencer par construire notre titre :

callback.c
Sélectionnez
static void set_title (void)
{
  if (docs.actif)
  {
    gchar *title = NULL;
    gchar *tmp = NULL;

    if (docs.actif->chemin)
    {
      tmp = g_path_get_basename (docs.actif->chemin);
    }
    else
    {
      tmp = g_strdup ("Nouveau document");
    }
    if (docs.actif->sauve)
    {
      title = g_strdup (tmp);
    }
    else
    {
      title = g_strdup_printf ("%s *", tmp);
    }
    g_free (tmp), tmp = NULL;
    /* ... */
    g_free (title), title = NULL;
  }
}

Quelques lignes de code simplifiées grâce aux fonctions de la glib. Nous obtenons donc notre titre dans la variable titre qui nous reste plus qu'à insérer dans l'onglet :

callback.c
Sélectionnez
    {
      gint index = 0;
      GtkWidget *p_child = NULL;
      GtkLabel *p_label = NULL;

      index = gtk_notebook_get_current_page (docs.p_notebook);
      p_child = gtk_notebook_get_nth_page (docs.p_notebook, index);
      p_label = GTK_LABEL (gtk_notebook_get_tab_label (docs.p_notebook, p_child));
      gtk_label_set_text (p_label, title);
    }

Cela peut paraître bizarre mais modifier le titre d'un onglet n'est pas trivial, il faut commencer par récupérer le numéro de la page en cours pour obtenir le widget qui est affiché dans l'onglet (car nous sommes libre de mettre le widget de notre choix), et comme dans notre cas, c'est un simple GtkLabel, on utilise la fonction gtk_label_set_text pour mettre à jour le titre. Pour finir, il faut faire appel à la fonction set_title lorsqu'on crée, ouvre, sauvegarde ou modifie un document, c'est à dire respectivement dans les fonctions cb_new, open_file, cb_save et cb_modifie.
Et voilà, c'est tout ! Moyennant quelques efforts de réflexion au début, le changement entre un éditeur simple et multi-documents est extrêmement simple et a même simplifié notre code : par exemple, pour la création du menu, nous n'avons plus besoin du paramètre user_data (pour simplifier, et au cas où nous en aurions de nouveau besoin, nous passons la valeur NULL).

XVI-G. Code source


précédentsommairesuivant

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2006-2008 Nicolas Joseph. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.