Aller au menu - Aller au contenu

Icône TP: Création d'un éditeur de texte

Avatar
Mise à jour : 31/07/2009
Difficulté : Intermédiaire Intermédiaire Creative Commons BY-NC-SA
808 visites depuis 7 jours, dont 45 sur ce chapitre classé 153/786
Dans ce chapitre, vous ferrez, après quelques indications, un éditeur de texte. Vous avez acquis toutes les connaissances nécessaires pour en faire un. Voilà ce que ça donne:
Image utilisateur

Hé ! Ce n'est pas le même que celui dans l'introduction de cette partie. Tu as changé d'avis ?

Vous n'êtes pas en mesure de faire un éditeur comme celui dans l'introduction de cette partie.
Donc, vous ne pourrez ni faire un panneau à gauche pour afficher les fichiers du disque dur, ni gérer l'impression, ni de mettre des onglets.
Après le chapitre 8, vous serez en mesure de terminer ce TP.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Instructions et conseils

Avant de vous faire faire ce TP, je dois vous donner quelques informations qui vous seront très utiles.

Pour les onglets


Les onglets... Tu as encore changé d'avis ? Qu'est-ce que se passe ?

Non, je n'ai pas changé d'avis. Cependant, vous ajouterez des onglets à ce programme lorsque vous saurez comment faire. De plus, si vous codez votre programme sans prendre en compte qu'il y aura des onglets plus tard, ce sera très long à modifier.

Je vais donc vous demander de créer un classe en plus de la classe principale du programme:
Code : Python
1
2
3
4
5
class DocumentOuvert():
	def __init__(self):
		self.chemin = ''
		self.aSauvegarder = False
		self.zoneTexteDocument = None

On créera donc un objet DocumentOuvert() chaque fois qu'un nouveau fichier est ouvert ou créé.

D'après cette classe, chaque document ouvert à son propre chemin (dans l'ordinateur, exemple: /home/antoyo/fichier), son propre status (à sauvegarder ou non) et sa propre zone de texte.
Pour obtenir le nom du fichier à partir de son chemin, utiliser la méthode os.path.basename, donc pensez à importer os.

Voici un exemple :
Code : Python
1
nomFichier = os.path.basename('/home/antoyo/fichier.txt')

Ici, nomFichier aura la valeur 'fichier.txt'.
Ceci peut être utile pour changer le titre de la fenêtre (et, plus tard, des onglets).

Chaque tampon de zone de texte a un signal 'changed' qui est émit lorsque le texte de la zone de texte est modifié.

Je vous conseille également de créer deux attributs à votre classe principale:
Code : Python
1
2
self.documents = []
self.documentActif = None

self.documents contiendra tous les documents ouverts (donc, des objets DocumentOuvert()). Donc, cette liste contiendra 0 ou 1 élément pour ce TP. Lorsque nous le ferons de nouveau avec des onglets, il pourra en contenir une infinité !
self.documentActif est le document actif qui contient en fait un objet DocumentOuvert().

Pour la fermeture du programme


Je vous demande de faire une boîte de dialogue qui demande à l'utilisateur s'il veut enregistrer ou non ou bien annuler la fermeture du fichier ouvert lorsque celui-ci a été modifié et que l'utilisateur demande la fermeture du programme.

Cependant, lorsque l'on ferme la fenêtre et que l'on appelle une fonction qui demande à afficher cette boîte de dialogue, la fenêtre se fermera quoi qu'il arrive.

On utilisera donc un signal de plus sur la fenêtre : 'delete-event'.
Dans la fonction callback de ce signal, si l'on retourne True, la fenêtre ne se fermera pas. J'ai utilisé ce signal en plus de 'destroy'.
Alors, la fonction callback du signal 'delete-event' ne fait que renvoyer que True pour dire que la fenêtre doit rester ouverte, ou False, pour dire que la fenêtre peut être fermé. Donc, vos méthodes devront renvoyer des valeurs pour signaler qu'un fichier s'est bien enregistrer ou non, par exemple.
Et dans la fonction callback du signal 'destroy', on appelle gtk.main_quit() si tous les fichiers se sont sauvegardés.

Pour les bouton et menu Quitter, vous n'avez pas le même problème.

Et un autre détail :
si aucun document n'est ouvert, les barres de défilement seront vide (donc, il n'y aura pas toujours une zone de texte dedans).

Fonctions du programme


Avant de vous laissez coder, je dois encore vous dire une chose. Ce que le programme que vous allez faire doit faire :)
  • Pouvoir ouvrir un fichier ;
  • Pouvoir créer un nouveau fichier (dans les deux premiers cas, vous devez créer une nouvelle zone de texte et l'ajouter dans les barres de défilement) ;
  • Enregistrer un fichier ;
  • Enregistrer un fichier sous un autre nom (si un fichier n'est pas sur le disque dur - quand le chemin est vide - vous devez l'enregistrer sous quand même) ;
  • Afficher la boîte de dialogue À propos ;
  • Afficher un menu Fichier qui contient les MenuItems suivants :
    • Nouveau ;
    • Ouvrir ;
    • Enregistrer ;
    • Enregistrer sous ;
    • Fermer ;
    • Quitter.

    et un menu Aide qui contient le MenuItem suivant :
    • À propos.

  • Afficher une barre d'outils contenant tous ces objets ;
  • Demander la confirmation avant d'écraser un fichier déjà existant ;
  • Afficher un avertissement si l'utilisateur essaye de créer un nouveau fichier lorsqu'un autre est ouvert ;
  • Afficher un avertissement si l'utilisateur essaye d'enregistrer si aucun fichier n'est ouvert ;
  • Afficher un avertissement si l'utilisateur essaye de fermer un fichier si aucun fichier n'est ouvert ;
  • Demander si l'on doit enregistrer un fichier modifié si l'utilisateur le ferme ou quitte le programme ;
  • Changer le titre de la fenêtre en fonction du nom du fichier. Si un nouveau document est ouvert, le titre est 'Nouveau document', si aucun document n'est ouvert, c'est 'Éditeur de texte en PyGtk'. Si le fichier est modifié (par rapport à l'ouverture ou à la création), on doit ajouter une étoile devant (par exemple: '*Nouveau document') ;
  • Afficher une erreur si l'utilisateur essaie d'ouvrir un fichier inexistant (utiliser les exceptions).
    Si vous voulez mettre un mot en évidence (en fait, utiliser le Pango Markup Language), par exemple le nom du fichier, il faut utiliser la méthode set_markup(str) sur la boîte de dialogue pour insérer le texte et il est donc inutile de spécifier le paramètre texte dans la construction de la boîte de dialogue.


Ça fait beaucoup, mais je sais que vous en êtes capables.
Vous devriez être capable de faire de TP, mais si vous avez de la difficulté, consultez les deux chapitres précédents et demandez de l'aide sur le forum.
Bonne chance ;)

Correction

Avez-vous tous réussi ?
Si ce n'est pas le cas, relisez les deux chapitres précédents et demandez de l'aide sur le forum avant de consulter la correction.

Nous allons regarder pas à pas mon code ; il est très certain que nos codes soient différents.

Pour commencer, j'importe les modules :
Code : Python
1
2
3
4
5
# -*- coding:Utf-8 -*-
import pygtk
pygtk.require("2.0")
import gtk
import os


Deuxièmement, je crée la classe DocumentOuvert() :
Code : Python
1
2
3
4
5
6
class DocumentOuvert():
	"Objet qui contient des informations sur un fichier ouvert"
	def __init__(self):
		self.chemin = ''
		self.aSauvegarder = False
		self.zoneTexteDocument = None

Je n'ai toujours pas de commentaire à faire ; ceci est expliqué plus haut.

Avant de continuer, je vais vous donner mon fichier glade :
Code : XML
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
<?xml version="1.0"?>
<interface>
  <requires lib="gtk+" version="2.16"/>
  <!-- interface-naming-policy project-wide -->
  <object class="GtkWindow" id="fenetre">
    <property name="visible">True</property>
    <property name="title" translatable="yes">&#xC9;diteur de texte en PyGtk</property>
    <property name="icon">editeurDeTexteIco.png</property>
    <accel-groups>
      <group name="accelgroup1"/>
    </accel-groups>
    <signal name="destroy" handler="quitter"/>
    <signal name="delete_event" handler="detruireFen"/>
    <child>
      <object class="GtkVBox" id="boiteV">
        <property name="visible">True</property>
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkMenuBar" id="barreMenu">
            <property name="visible">True</property>
            <child>
              <object class="GtkMenuItem" id="menuitem1">
                <property name="visible">True</property>
                <property name="label" translatable="yes">_Fichier</property>
                <property name="use_underline">True</property>
                <child type="submenu">
                  <object class="GtkMenu" id="menu1">
                    <property name="visible">True</property>
                    <child>
                      <object class="GtkImageMenuItem" id="nouveau">
                        <property name="label">gtk-new</property>
                        <property name="visible">True</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                        <property name="accel_group">accelgroup1</property>
                        <signal name="activate" handler="nouveau"/>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem" id="ouvrir">
                        <property name="label">gtk-open</property>
                        <property name="visible">True</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                        <property name="accel_group">accelgroup1</property>
                        <signal name="activate" handler="ouvrirFichierDialogue"/>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem" id="enregistrer">
                        <property name="label">gtk-save</property>
                        <property name="visible">True</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                        <property name="accel_group">accelgroup1</property>
                        <signal name="activate" handler="enregistrer"/>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem" id="enregistrerSous">
                        <property name="label">gtk-save-as</property>
                        <property name="visible">True</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                        <property name="accel_group">accelgroup1</property>
                        <signal name="activate" handler="enregistrerSous"/>
                      </object>
                    </child>
                    <child>
                      <object class="GtkSeparatorMenuItem" id="separatormenuitem1">
                        <property name="visible">True</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem" id="fermer">
                        <property name="label">gtk-close</property>
                        <property name="visible">True</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                        <property name="accel_group">accelgroup1</property>
                        <signal name="activate" handler="fermer"/>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem" id="quitter">
                        <property name="label">gtk-quit</property>
                        <property name="visible">True</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                        <property name="accel_group">accelgroup1</property>
                        <signal name="activate" handler="quitter"/>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
            </child>
            <child>
              <object class="GtkMenuItem" id="menuitem4">
                <property name="visible">True</property>
                <property name="label" translatable="yes">Aid_e</property>
                <property name="use_underline">True</property>
                <child type="submenu">
                  <object class="GtkMenu" id="menu3">
                    <property name="visible">True</property>
                    <child>
                      <object class="GtkImageMenuItem" id="aPropos">
                        <property name="label">gtk-about</property>
                        <property name="visible">True</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                        <property name="accel_group">accelgroup1</property>
                        <signal name="activate" handler="aPropos"/>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkToolbar" id="barreOutils">
            <property name="visible">True</property>
            <child>
              <object class="GtkToolButton" id="nouveau1">
                <property name="visible">True</property>
                <property name="label" translatable="yes">Nouveau</property>
                <property name="use_underline">True</property>
                <property name="stock_id">gtk-new</property>
                <signal name="clicked" handler="nouveau"/>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="homogeneous">True</property>
              </packing>
            </child>
            <child>
              <object class="GtkToolButton" id="toolbutton1">
                <property name="visible">True</property>
                <property name="label" translatable="yes">Ouvrir</property>
                <property name="use_underline">True</property>
                <property name="stock_id">gtk-open</property>
                <signal name="clicked" handler="ouvrirFichierDialogue"/>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="homogeneous">True</property>
              </packing>
            </child>
            <child>
              <object class="GtkToolButton" id="toolbutton2">
                <property name="visible">True</property>
                <property name="label" translatable="yes">Enregistrer</property>
                <property name="use_underline">True</property>
                <property name="stock_id">gtk-save</property>
                <signal name="clicked" handler="enregistrer"/>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="homogeneous">True</property>
              </packing>
            </child>
            <child>
              <object class="GtkToolButton" id="toolbutton3">
                <property name="visible">True</property>
                <property name="label" translatable="yes">Enregistrer sous...</property>
                <property name="use_underline">True</property>
                <property name="stock_id">gtk-save-as</property>
                <signal name="clicked" handler="enregistrerSous"/>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="homogeneous">True</property>
              </packing>
            </child>
            <child>
              <object class="GtkSeparatorToolItem" id="separateur">
                <property name="visible">True</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="homogeneous">True</property>
              </packing>
            </child>
            <child>
              <object class="GtkToolButton" id="toolbutton5">
                <property name="visible">True</property>
                <property name="label" translatable="yes">&#xC0; propos</property>
                <property name="use_underline">True</property>
                <property name="stock_id">gtk-about</property>
                <signal name="clicked" handler="aPropos"/>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="homogeneous">True</property>
              </packing>
            </child>
            <child>
              <object class="GtkToolButton" id="fermer1">
                <property name="visible">True</property>
                <property name="label" translatable="yes">Fermer</property>
                <property name="use_underline">True</property>
                <property name="stock_id">gtk-close</property>
                <signal name="clicked" handler="fermer"/>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="homogeneous">True</property>
              </packing>
            </child>
            <child>
              <object class="GtkToolButton" id="toolbutton4">
                <property name="visible">True</property>
                <property name="label" translatable="yes">Quitter</property>
                <property name="use_underline">True</property>
                <property name="stock_id">gtk-quit</property>
                <signal name="clicked" handler="quitter"/>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="homogeneous">True</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkScrolledWindow" id="barresDefilement">
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="hscrollbar_policy">automatic</property>
            <property name="vscrollbar_policy">automatic</property>
            <child>
              <placeholder/>
            </child>
          </object>
          <packing>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
  <object class="GtkAccelGroup" id="accelgroup1"/>
</interface>


Ensuite, on crée la fenêtre principale à partir du fichier glade :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class PyGtkApp:
	def __init__(self):
		#Création de la fenêtre principale à partie d'un fichier glade
		interface = gtk.Builder()
		interface.add_from_file('fenetre.glade')
		
		self.fenetre = interface.get_object("fenetre")
		self.barresDefilement = interface.get_object('barresDefilement')
		interface.connect_signals(self)
		
		self.fenetre.maximize()
		
		self.documents = [] #Liste des documents ouverts
		self.documentActif = None #Le document actif (objet DocumentOuvert())
[...]

PyGtkApp()
gtk.main()


Tout ceci est du déjà vu : on crée la fenêtre, on connecte les signaux, on met les barres de défilement dans une variable pour réutiliser le widget et on crée deux attributs. Après les autres fonctions que j'ai passés pour l'instant, on appelle la classe principale et on commence la boucle principale.

Voyons chacune des méthodes de la classe principale :
Code : Python
1
2
3
4
5
6
7
8
def detruireFen(self, widget, evenement):
		"Lorsque la fenêtre est détruite"
		if self.documentActif != None:
			if not self.fermer(None) == False:
				return False #Si le document est bien fermé, on peut fermer la fenêtre
		else:
			return False #Si aucun document n'est actif, on peut aussi la fermer
	 	return True #Sinon, on la laisse ouverte

Cette méthode est appelée lorsque l'utilisateur clique sur le x ou ferme la fenêtre d'une autre façon (par exemple, Alt-F4). Si aucun document n'est actif, on revoie False, donc le signal 'destroy' est émit. Ce signal est également émit si un document est ouvert mais qu'il se ferme ensuite. Sinon, on renvoie True pour laisser la fenêtre ouverte, donc ne pas émettre le signal 'destroy'.

Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def aPropos(self, widget):
		"Dialogue À propos"
		pixbuf = gtk.gdk.pixbuf_new_from_file("editeurDeTexteIco.png")
		aPropos = gtk.AboutDialog()
		aPropos.set_icon_from_file("editeurDeTexteIco.png")
		aPropos.set_name("Éditeur de texte")
		aPropos.set_version("1.0")
		aPropos.set_copyright("Copyright © 2009, tous droits réservés")
		aPropos.set_comments(aPropos.get_name() + " " + aPropos.get_version() +" est un éditeur de texte basique.")
		aPropos.set_license("Copyright © 2009 antoyo\n\nThis program is free software; you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation; either version 2 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along\nwith this program; if not, write to the Free Software Foundation, Inc.,\n51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.")
		auteurs = ["Antoyo"]
		aPropos.set_authors(auteurs)
		aPropos.set_logo(pixbuf)
		reponse = aPropos.run()
		aPropos.destroy()

Revoyez le chapitre Les widgets (Partie 2), sous-partie D'autres boîtes de dialogue pour savoir ce que fait chacune des méthodes.
Notez également que j'ai mis la licence GNU GPL ; regardez ce tutoriel pour faire de votre programme un logiciel libre.

Voici enfin la fonction de fermeture d'un fichier :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def fermer(self, widget):
		"Fermeture d'un fichier" #Dans cette fonction, si le retour est False, cela indique qu'il n'y a pas eu de fermeture
		if self.documentActif != None: #S'il y a un document actif
			if self.documentActif.aSauvegarder: #Si il est modifié
				#On crée un boîte de dialogue pour demander à l'utilisateur s'il veut enregistrer les modifications ou non ou bien annuler la fermeture
				fermetureDialogue = gtk.Dialog("Enregistrer", self.fenetre, 0, (gtk.STOCK_YES, gtk.RESPONSE_YES, gtk.STOCK_NO, gtk.RESPONSE_NO, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
				
				etiquette = gtk.Label('Voulez-vous enregistrer les modifications ?')
				boiteH = gtk.HBox()
				fermetureDialogue.vbox.pack_start(boiteH, False)
				stock = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG)
				boiteH.pack_start(stock, False)
				boiteH.pack_start(etiquette, False)
				
				fermetureDialogue.show_all()
				
				reponse = fermetureDialogue.run()
				if reponse == gtk.RESPONSE_YES:
					if self.enregistrer(None):
						#Si l'utilisateur appuie sur Oui et qu'il enregistre le document, on peut le fermer
						self.documentActif.zoneTexteDocument.destroy()
						
						self.documents.remove(self.documentActif)
						self.documentActif = None
					else:
						#S'il ne l'enregistre pas, on ferme la boîte de dialogue
						fermetureDialogue.destroy()
						return False
					self.fenetre.set_title('Éditeur de texte en PyGtk') #On remet le titre à la fenêtre
				elif reponse == gtk.RESPONSE_NO:
					#S'il appuie sur Non, on ferme le document sans enregistrer
					self.documentActif.zoneTexteDocument.destroy()
					
					self.documents.remove(self.documentActif)
					self.documentActif = None
					self.fenetre.set_title('Éditeur de texte en PyGtk')
				elif reponse == gtk.RESPONSE_CANCEL:
					#S'il appuie sur Annuler, on fermer seulement la boîte de dialogue
					fermetureDialogue.destroy()
					return False
				fermetureDialogue.destroy()
			else:
				self.documentActif.zoneTexteDocument.destroy()
				self.documents.remove(self.documentActif)
				self.documentActif = None
				self.fenetre.set_title('Éditeur de texte en PyGtk')
		else:
			self.afficherAvertissement("Il n'y a aucun document ouvert.")

Voyons-le bloc par bloc. Premièrement :
Code : Python
1
2
3
4
if self.documentActif != None: #S'il y a un document actif
[...]
else:
	self.afficherAvertissement("Il n'y a aucun document ouvert.")

Au début, on vérifie si un document est ouvert ; si ce n'est pas le cas, on affiche un avertissement pour l'indiquer à l'utilisateur (il faut prévoir tous les cas en programmation ; on ne sait jamais si un ****** essayera de fermer un fichier alors qu'aucun n'est ouvert.)

Ensuite, nous voyons ceci :
Code : Python
1
2
3
4
5
6
7
if self.documentActif.aSauvegarder: #Si il est modifié
[...]
else:
	self.documentActif.zoneTexteDocument.destroy()
	self.documents.remove(self.documentActif)
	self.documentActif = None
	self.fenetre.set_title('Éditeur de texte en PyGtk')


Donc, si un fichier est ouvert, on vérifie s'il est modifié ; si ce n'est pas le cas, on détruit la zone de texte, on enlève le document actif de la liste des documents et on change le titre de la fenêtre.

Après, il y a :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#On crée un boîte de dialogue pour demander à l'utilisateur s'il veut enregistrer les modifications ou non ou bien annuler la fermeture
fermetureDialogue = gtk.Dialog("Enregistrer", self.fenetre, 0, (gtk.STOCK_YES, gtk.RESPONSE_YES, gtk.STOCK_NO, gtk.RESPONSE_NO, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))

etiquette = gtk.Label('Voulez-vous enregistrer les modifications ?')
boiteH = gtk.HBox()
fermetureDialogue.vbox.pack_start(boiteH, False)
stock = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG)
boiteH.pack_start(stock, False)
boiteH.pack_start(etiquette, False)

fermetureDialogue.show_all()

reponse = fermetureDialogue.run()


S'il est modifié, on affiche une boîte de dialogue personnalisée pour demander gentiment à l'utilisateur ce qu'il veut faire avec le fichier ouvert (l'enregistrer, ne pas l'enregistrer ou annuler la fermeture).

Puis, vient cela :
Code : Python
1
2
3
4
5
6
7
8
if reponse == gtk.RESPONSE_YES:
[...]
elif reponse == gtk.RESPONSE_NO:
[...]
elif reponse == gtk.RESPONSE_CANCEL:
	#S'il appuie sur Annuler, on fermer seulement la boîte de dialogue
	fermetureDialogue.destroy()
	return False


Si l'utilisateur appuie sur Annuler, on détruit la boîte de dialogue sans autre action et on retourne False pour indiquer qu'aucun enregistrement n'a été effectué.

Revenons à ceci :
Code : Python
1
2
3
4
5
6
7
elif reponse == gtk.RESPONSE_NO:
	#S'il appuie sur Non, on ferme le document sans enregistrer
	self.documentActif.zoneTexteDocument.destroy()
	
	self.documents.remove(self.documentActif)
	self.documentActif = None
	self.fenetre.set_title('Éditeur de texte en PyGtk')


S'il appuie sur Non, on détruit le fichier de la même manière que s'il n'aurait pas été à sauvegarder.

Et enfin :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if reponse == gtk.RESPONSE_YES:
	if self.enregistrer(None):
		#Si l'utilisateur appuie sur Oui et qu'il enregistre le document, on peut le fermer
		self.documentActif.zoneTexteDocument.destroy()
		
		self.documents.remove(self.documentActif)
		self.documentActif = None
	else:
		#S'il ne l'enregistre pas, on ferme la boîte de dialogue
		fermetureDialogue.destroy()
		return False
	self.fenetre.set_title('Éditeur de texte en PyGtk') #On remet le titre à la fenêtre


Par contre, s'il appuie sur Oui, on fait quelque chose de plus intéressant ! On appelle la méthode self.enregistrer() (voir plus bas) et si l'utilisateur a changé d'avis (s'il a appuyé sur Annuler), ben on ferme la boîte de dialogue. S'il a choisi le nom du fichier (méthode self.enregistrer()), on ferme le document ; la méthode self.enregistrer() faisant le travail d'enregistrer le fichier.
Le retour de cette méthode est très important : il permet de dire aux méthodes qui l'appelle si le fichier a été fermé, donc si elles peuvent continuer à faire ce qu'elles doivent faire.


Puis, voyons la méthode self.nouveau() :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def nouveau(self, widget):
		"Création d'un nouveau document"
		if self.documentActif == None: #On le crée seulement s'il n'y en a pas déjà un d'ouvert
			#Création d'un objet DocumentOuvert() sans chemin
			nouveau = DocumentOuvert()
			nouveau.chemin = ''
			nouveau.ASauvegarder = False
			#Le chemin vaut '' et il n'est pas à sauvegarder
			self.documents.append(nouveau) #On l'ajoute à la liste
			self.documentActif = nouveau #Et c'est le document actif
		
			nouveau.zoneTexteDocument = gtk.TextView() #On crée une nouvelle zone de texte
			tampon = nouveau.zoneTexteDocument.get_buffer()
			tampon.connect("changed", self.fichierModifie) #On récupère le tampon pour savoir quand il est modifié
			self.barresDefilement.add(nouveau.zoneTexteDocument)
			nouveau.zoneTexteDocument.show()
		
			self.definirTitre()
		else:
			self.afficherAvertissement("Il y a déjà un document ouvert.")

Si un document est déjà ouvert, on avertie l'utilisateur que c'est le cas. Sinon, on crée un objet DocumentOuvert(), en spécifiant que son chemin est '' et qu'il n'est pas à enregistrer. On l'ajoute à la liste des documents et le nouveau document devient le document actif. On crée un nouvelle zone de texte et on récupère son buffer. On le connecte au signal 'changed' (rappelez-vous, celui qui est émit lorsque le contenu d'un buffer est changé), on l'ajoute dans des barres de défilement et on l'affiche.

La méthode self.fichierModifie est très simple :
Code : Python
1
2
3
4
def fichierModifie(self, widget):
		#Si un fichier est modifié, on change son status et le titre de la fenêtre
		self.documentActif.aSauvegarder = True
		self.definirTitre()

Elle est appelé lorsque le signal 'changed' est émit. Dans cette méthode, on change l'attribut aSauvegarder du document actif pour True et on change le titre de la fenêtre (cela ajoute la petite étoile signifiant que le fichier a été modifié).

Voyons maintenant la méthode self.definirTitre() :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def definirTitre(self):
		titre = ''
		if self.documentActif.chemin != '':
			#Si un document est ouvert, on affiche son nom
			titre = os.path.basename(self.documentActif.chemin)
		else:
			#Si c'est un nouveau document, on affiche 'Nouveau document'
			titre = 'Nouveau document'
		if self.documentActif.aSauvegarder:
			#S'il est modifié, on ajoute une étoile
			titre = '*' + titre
		self.fenetre.set_title(titre)

Si le fichier ouvert existe sur l'ordinateur, le titre devient le nom de ce fichier, sinon, on l'appelle 'Nouveau document'. S'il est modifié, on ajoute une étoile. Finalement, on change le titre de la fenêtre.

Voilà maintenant une méthode très importante dans un éditeur de texte, la méthode self.enregistrer() :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def enregistrer(self, widget):
		#Retourne False si l'utilisateur n'effectue pas la sauvegarde, sinon retourne True
		if self.documentActif != None:
			if self.documentActif.aSauvegarder:
				if self.documentActif.chemin == '':
					#Si un document est ouvert, modifié et n'existe pas dans l'ordinateur, on affiche la boîte de dialogue Enregistrer sous
					sauvegarderFichierDialogue = gtk.FileChooserDialog("Enregistrer sous...", self.fenetre, gtk.FILE_CHOOSER_ACTION_SAVE, ((gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)))
					sauvegarderFichierDialogue.set_do_overwrite_confirmation(True)
					reponse = sauvegarderFichierDialogue.run()
					
					if reponse == gtk.RESPONSE_OK:
						nomFichier = sauvegarderFichierDialogue.get_filename()
						self.documentActif.chemin = nomFichier
						sauvegarderFichierDialogue.destroy()
					elif reponse == gtk.RESPONSE_CANCEL:
						sauvegarderFichierDialogue.destroy()
						return False
				if self.documentActif.chemin != '':
					fichier = open(self.documentActif.chemin, 'w') #On ouvre le fichier
					
					#On obtient le texte de la zone de texte
					tampon = self.documentActif.zoneTexteDocument.get_buffer()
					debut = tampon.get_start_iter()
					fin = tampon.get_end_iter()
					texte = tampon.get_text(debut, fin, True)
					
					#On enregistre le fichier
					fichier.write(texte)
					fichier.close()
					#Il n'est plus à sauvegarder, donc on change le titre (plus d'étoile)
					self.documentActif.aSauvegarder = False
					self.definirTitre()
					return True
			else:
				if self.fenetre.get_title() == 'Nouveau document':
					#Si le document n'est pas modifié, mais que c'est un nouveau document, on peut l'enregistrer
					self.documentActif.aSauvegarder = True
					if not self.enregistrer(None):
						#Si l'utilisateur ne le sauvegarde pas
						self.documentActif.aSauvegarder = False
			self.definirTitre()
		else:
			self.afficherAvertissement("Il n'y a aucun document ouvert.")

Voyons cette méthode bloc par bloc ; ce sera plus facile à expliquer.
Code : Python
1
2
3
4
if self.documentActif != None:
[...]
else:
	self.afficherAvertissement("Il n'y a aucun document ouvert.")

Premièrement, il faut qu'un document soit ouvert, sinon, on affiche un avertissement.

Voici le code qui suit :
Code : Python
1
2
3
4
5
6
7
8
9
if self.documentActif.aSauvegarder:
[...]
else:
	if self.fenetre.get_title() == 'Nouveau document':
		#Si le document n'est pas modifié, mais que c'est un nouveau document, on peut l'enregistrer
		self.documentActif.aSauvegarder = True
		if not self.enregistrer(None):
			#Si l'utilisateur ne le sauvegarde pas
			self.documentActif.aSauvegarder = False


Deuxièmement, si le fichier n'est pas modifié, on vérifie si c'est un nouveau document ; si c'est le cas, on indique qu'il est à sauvegarder et on appelle cette même méthode. S'il a changé d'avis et ne veut pas enregistrer, on remet l'attribut aSauvegarder à False (pour qu'il n'y ait pas d'étoile dans le titre ; j'en ai eu des problèmes avec cette fichue étoile :D ).

La suite est :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if self.documentActif.chemin == '':
	#Si un document est ouvert, modifié et n'existe pas dans l'ordinateur, on affiche la boîte de dialogue Enregistrer sous
	sauvegarderFichierDialogue = gtk.FileChooserDialog("Enregistrer sous...", self.fenetre, gtk.FILE_CHOOSER_ACTION_SAVE, ((gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)))
	sauvegarderFichierDialogue.set_do_overwrite_confirmation(True)
	reponse = sauvegarderFichierDialogue.run()
	
	if reponse == gtk.RESPONSE_OK:
		nomFichier = sauvegarderFichierDialogue.get_filename()
		self.documentActif.chemin = nomFichier
		sauvegarderFichierDialogue.destroy()
	elif reponse == gtk.RESPONSE_CANCEL:
		sauvegarderFichierDialogue.destroy()
		return False


Après, on vérifie que le fichier existe sur le disque dur ; si ce n'est pas le cas, on affiche une boîte de dialogue dans laquelle on demande à l'utilisateur le nom du fichier à enregistrer. N'oubliez pas d'appeler la méthode sauvegarderFichierDialogue.set_do_overwrite_confirmation(True) (au début, je l'avais oublié celle-là ; j'avais donc recréé cette méthode, mais ça fonctionnait :euh: )

Ici, on n'enregistre pas tout de suite le fichier, on indique seulement son chemin.

Et enfin :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
if self.documentActif.chemin != '':
	fichier = open(self.documentActif.chemin, 'w') #On ouvre le fichier
	
	#On obtient le texte de la zone de texte
	tampon = self.documentActif.zoneTexteDocument.get_buffer()
	debut = tampon.get_start_iter()
	fin = tampon.get_end_iter()
	texte = tampon.get_text(debut, fin, True)
	
	#On enregistre le fichier
	fichier.write(texte)
	fichier.close()
	#Il n'est plus à sauvegarder, donc on change le titre (plus d'étoile)
	self.documentActif.aSauvegarder = False
	self.definirTitre()
	return True


C'est dans cette condition-là qu'on enregistre : d'abord, on ouvre le fichier, ensuite, on récupère le contenu du gtk.TextView(), on écrit dans le fichier, on change l'attribut aSauvegarder et le titre (pour enlever l'étoile).

Il ne reste pas beaucoup de méthode, allez courage :D (je commence à avoir mal au doigt, vous savez :p )

Voici la méthode self.enregistrerSous() :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def enregistrerSous(self, widget):
		if self.documentActif != None: #Si un document est ouvert
			#On enregistre des informations temporaires au cas où l'utilisateur annulerait la sauvegarde			
			cheminTemp = self.documentActif.chemin
			aSauvegarderTemp = self.documentActif.aSauvegarder
			
			#On change le chemin pour '' et aSauvegarde pour True pour que l'enregistrement se fasse dans un autre fichier (enregistrer sous)
			self.documentActif.chemin = ''
			self.documentActif.aSauvegarder = True
			if not self.enregistrer(widget): #Si l'utilisateur annule l'enregistrement, on remet les informations d'avant
				self.documentActif.chemin = cheminTemp
				self.documentActif.aSauvegarder = aSauvegarderTemp
				self.definirTitre() #et on remet le titre (parce qu'on le change après l'enregistrement)
		else:
			self.afficherAvertissement("Il n'y a aucun document ouvert.")

Si aucun document n'est ouvert, on affiche un avertissement !
Sinon, on enregistre les attributs du document dans des variables temporaires, car on doit les changer pour appeler la méthode self.enregistrer(). En effet, si le chemin vaut '' et que aSauvegarder, True, cette méthode affiche la boîte de dialogue 'Enregistrer sous...'.

Par contre, si aucun enregistrement n'est effectué, on remet leur valeur initiale au attributs changés. On le sait grâce à ce que retourne la méthode self.enregistrer().

Voilà maintenant la méthode self.quitter() :
Code : Python
1
2
3
4
5
6
7
8
def quitter(self, widget, evenement = None):
		if self.documentActif != None:
			if not self.fermer(None) == False:
				#Si un document est ouvert, mais qu'il est fermé après, on quitter
				gtk.main_quit()
		else:
			#Si aucun document est ouvert, on quitte
			gtk.main_quit()

Elle est appelée non seulement lorsque l'utilisateur clique sur le menu Quitter, ou le bouton Quitter sur la barre d'outils, mais aussi si la méthode self.detruireFen() revoie False (dans le cas où le fichier s'est fermé).

Si un document est ouvert, on essaie de le fermer, si ça fontionne, on quitte. Si aucun document n'est ouvert, on quitte aussi.
Pourquoi as-tu écrit "if not self.fermer(None) == False:" au lieu de "if self.fermer(None):", ça aurait été moins long, non ?

J'ai écrit "if not self.fermer(None) == False:" parce que la fonction ne renvoie pas True, j'aurais pu changé cela, en effet.

Voici la méthode self.ouvrirFichier() :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def ouvrirFichier(self, nomFichier):
		#Cette méthode ressemble à la méthode self.nouveau()
		#Création d'un objet DocumentOuvert() avec chemin
		nouveau = DocumentOuvert()
		nouveau.chemin = nomFichier
		nouveau.ASauvegarder = False
		self.documents.append(nouveau)
		self.documentActif = nouveau
	
		nouveau.zoneTexteDocument = gtk.TextView()
		nouveau.zoneTexteDocument.show()
		self.barresDefilement.add(nouveau.zoneTexteDocument)
		tampon = nouveau.zoneTexteDocument.get_buffer()
		
		fichier = open(nomFichier, 'r')
		texteListe = fichier.readlines()
		texte = ''.join(texteListe)
		fichier.close()
		
		#Comme on doit changer le texte pour celui du fichier, il faut bien faire attention à connecter le signal 'changed' APRÈS de l'avoir changer pour pas que l'attribut aSauvegarder devienne True
		tampon.set_text(texte)
		tampon.connect("changed", self.fichierModifie)
		
		self.definirTitre()

Elle ressemble beaucoup à la méthode self.nouveau(). Les seules différences sont que le document a un chemin et que la zone de texte a du texte.

Cette méthode est appelée par une autre, self.ouvrirFichierDialogue :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def ouvrirFichierDialogue(self, widget):
		continuer = False
		if self.documentActif != None:
			if not self.fermer(None) == False:
				#Si le document ouvert se ferme, on peut ouvrir la boîte de dialogue
				continuer = True
		else:
			#Si aucun document est ouvert, on peut ouvrir la boîte de dialogue
			continuer = True
		if continuer:
			ouvrirFichierDialogue = gtk.FileChooserDialog("Ouverture de fichiers", self.fenetre, gtk.FILE_CHOOSER_ACTION_OPEN, ((gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)))
			reponse = ouvrirFichierDialogue.run()
			if reponse == gtk.RESPONSE_OK:
				nomFichier = ouvrirFichierDialogue.get_filename()
				#On essaie d'ouvrir le fichier pour savoir s'il existe
				try:
					fichier = open(nomFichier)
				except:
					#Dans le cas où l'utilisateur aurait taper le nom du fichier et que celui-ci n'existerait pas, on affiche une erreur
					self.afficherErreur('Le fichier <b>' + os.path.basename(nomFichier) + '</b> n\'existe pas.', True)
				else:
					#S'il existe, on l'ouvre
					fichier.close()
					self.ouvrirFichier(nomFichier)
			ouvrirFichierDialogue.destroy()

Si aucun document n'est ouvert, on passe à la suite, sinon, il faut l'enregistrer. Si l'enregistrement s'effectue correctement, on continue aussi.
Si l'utilisateur ouvre un fichier, on vérifie qu'il existe bien (avec les exceptions), s'il n'existe pas, on affiche une erreur.

Voici les dernières méthodes, dont une inutilisée qui pourra servir plus tard :
Code : Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def afficherInformation(self, texte, use_markup = False):
		"Affiche une information (non utilisée)"
		if use_markup:
			#Si use_markup vaut True, on insère le texte avec set_markup()
			infoDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
			infoDialogue.set_markup(texte)
		else:
			infoDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, texte)
		infoDialogue.set_title("Information")
		infoDialogue.run()
		infoDialogue.destroy()
	
	def afficherAvertissement(self, texte, use_markup = False):
		"Affiche un avertissement"
		if use_markup:
			avertissementDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK)
			avertissementDialogue.set_markup(texte)
		else:
			avertissementDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, texte)
		avertissementDialogue.set_title("Avertissement")
		avertissementDialogue.run()
		avertissementDialogue.destroy()
	
	def afficherErreur(self, texte, use_markup = False):
		"Affiche une erreur"
		if use_markup:
			erreurDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK)
			erreurDialogue.set_markup(texte)
		else:
			erreurDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, texte)
		erreurDialogue.set_title("Erreur")
		erreurDialogue.run()
		erreurDialogue.destroy()
		erreurDialogue.destroy()

La seule chose de différent par rapport à d'habitude, c'est que si le paramètre use_markup vaut True, on affiche le texte avec la méthode set_markup().

Voici le code complet (je le donne, car il y a un problème avec les tabulations) :
Code : Python
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# -*- coding:Utf-8 -*-
import pygtk
pygtk.require("2.0")
import gtk
import os

class DocumentOuvert():
	"Objet qui contient des informations sur un fichier ouvert"
	def __init__(self):
		self.chemin = ''
		self.aSauvegarder = False
		self.zoneTexteDocument = None

class PyGtkApp:
	def __init__(self):
		#Création de la fenêtre principale à partie d'un fichier glade
		interface = gtk.Builder()
		interface.add_from_file('fenetre.glade')
		
		self.fenetre = interface.get_object("fenetre")
		self.barresDefilement = interface.get_object('barresDefilement')
		interface.connect_signals(self)
		
		self.fenetre.maximize()
		
		self.documents = [] #Liste des documents ouverts
		self.documentActif = None #Le document actif (objet DocumentOuvert())
		
	def detruireFen(self, widget, evenement):
		"Lorsque la fenêtre est détruite"
		if self.documentActif != None:
			if not self.fermer(None) == False:
				return False #Si le document est bien fermé, on peut fermer la fenêtre
		else:
			return False #Si aucun document n'est actif, on peut aussi la fermer
	 	return True #Sinon, on la laisse ouverte
	
	def aPropos(self, widget):
		"Dialogue À propos"
		pixbuf = gtk.gdk.pixbuf_new_from_file("editeurDeTexteIco.png")
		aPropos = gtk.AboutDialog()
		aPropos.set_icon_from_file("editeurDeTexteIco.png")
		aPropos.set_name("Éditeur de texte")
		aPropos.set_version("1.0")
		aPropos.set_copyright("Copyright © 2009, tous droits réservés")
		aPropos.set_comments(aPropos.get_name() + " " + aPropos.get_version() +" est un éditeur de texte basique.")
		aPropos.set_license("Copyright © 2009 antoyo\n\nThis program is free software; you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation; either version 2 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along\nwith this program; if not, write to the Free Software Foundation, Inc.,\n51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.")
		auteurs = ["Antoyo"]
		aPropos.set_authors(auteurs)
		aPropos.set_logo(pixbuf)
		reponse = aPropos.run()
		aPropos.destroy()
	
	def fermer(self, widget):
		"Fermeture d'un fichier" #Dans cette fonction, si le retour est False, cela indique qu'il n'y a pas eu de fermeture
		if self.documentActif != None: #S'il y a un document actif
			if self.documentActif.aSauvegarder: #Si il est modifié
				#On crée un boîte de dialogue pour demander à l'utilisateur s'il veut enregistrer les modifications ou non ou bien annuler la fermeture
				fermetureDialogue = gtk.Dialog("Enregistrer", self.fenetre, 0, (gtk.STOCK_YES, gtk.RESPONSE_YES, gtk.STOCK_NO, gtk.RESPONSE_NO, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
				
				etiquette = gtk.Label('Voulez-vous enregistrer les modifications ?')
				boiteH = gtk.HBox()
				fermetureDialogue.vbox.pack_start(boiteH, False)
				stock = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG)
				boiteH.pack_start(stock, False)
				boiteH.pack_start(etiquette, False)
				
				fermetureDialogue.show_all()
				
				reponse = fermetureDialogue.run()
				if reponse == gtk.RESPONSE_YES:
					if self.enregistrer(None):
						#Si l'utilisateur appuie sur Oui et qu'il enregistre le document, on peut le fermer
						self.documentActif.zoneTexteDocument.destroy()
						
						self.documents.remove(self.documentActif)
						self.documentActif = None
					else:
						#S'il ne l'enregistre pas, on ferme la boîte de dialogue
						fermetureDialogue.destroy()
						return False
					self.fenetre.set_title('Éditeur de texte en PyGtk') #On remet le titre à la fenêtre
				elif reponse == gtk.RESPONSE_NO:
					#S'il appuie sur Non, on ferme le document sans enregistrer
					self.documentActif.zoneTexteDocument.destroy()
					
					self.documents.remove(self.documentActif)
					self.documentActif = None
					self.fenetre.set_title('Éditeur de texte en PyGtk')
				elif reponse == gtk.RESPONSE_CANCEL:
					#S'il appuie sur Annuler, on fermer seulement la boîte de dialogue
					fermetureDialogue.destroy()
					return False
				fermetureDialogue.destroy()
			else:
				self.documentActif.zoneTexteDocument.destroy()
				self.documents.remove(self.documentActif)
				self.documentActif = None
				self.fenetre.set_title('Éditeur de texte en PyGtk')
		else:
			self.afficherAvertissement("Il n'y a aucun document ouvert.")
	
	def nouveau(self, widget):
		"Création d'un nouveau document"
		if self.documentActif == None: #On le crée seulement s'il n'y en a pas déjà un d'ouvert
			#Création d'un objet DocumentOuvert() sans chemin
			nouveau = DocumentOuvert()
			nouveau.chemin = ''
			nouveau.ASauvegarder = False
			#Le chemin vaut '' et il n'est pas à sauvegarder
			self.documents.append(nouveau) #On l'ajoute à la liste
			self.documentActif = nouveau #Et c'est le document actif
		
			nouveau.zoneTexteDocument = gtk.TextView() #On crée une nouvelle zone de texte
			tampon = nouveau.zoneTexteDocument.get_buffer()
			tampon.connect("changed", self.fichierModifie) #On récupère le tampon pour savoir quand il est modifié
			self.barresDefilement.add(nouveau.zoneTexteDocument)
			nouveau.zoneTexteDocument.show()
		
			self.definirTitre()
		else:
			self.afficherAvertissement("Il y a déjà un document ouvert.")
	
	def fichierModifie(self, widget):
		#Si un fichier est modifié, on change son status et le titre de la fenêtre
		self.documentActif.aSauvegarder = True
		self.definirTitre()
	
	def definirTitre(self):
		titre = ''
		if self.documentActif.chemin != '':
			#Si un document est ouvert, on affiche son nom
			titre = os.path.basename(self.documentActif.chemin)
		else:
			#Si c'est un nouveau document, on affiche 'Nouveau document'
			titre = 'Nouveau document'
		if self.documentActif.aSauvegarder:
			#S'il est modifié, on ajoute une étoile
			titre = '*' + titre
		self.fenetre.set_title(titre)
	
	def enregistrer(self, widget):
		#Retourne False si l'utilisateur n'effectue pas la sauvegarde, sinon retourne True
		if self.documentActif != None:
			if self.documentActif.aSauvegarder:
				if self.documentActif.chemin == '':
					#Si un document est ouvert, modifié et n'existe pas dans l'ordinateur, on affiche la boîte de dialogue Enregistrer sous
					sauvegarderFichierDialogue = gtk.FileChooserDialog("Enregistrer sous...", self.fenetre, gtk.FILE_CHOOSER_ACTION_SAVE, ((gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)))
					sauvegarderFichierDialogue.set_do_overwrite_confirmation(True)
					reponse = sauvegarderFichierDialogue.run()
					
					if reponse == gtk.RESPONSE_OK:
						nomFichier = sauvegarderFichierDialogue.get_filename()
						self.documentActif.chemin = nomFichier
						sauvegarderFichierDialogue.destroy()
					elif reponse == gtk.RESPONSE_CANCEL:
						sauvegarderFichierDialogue.destroy()
						return False
				if self.documentActif.chemin != '':
					fichier = open(self.documentActif.chemin, 'w') #On ouvre le fichier
					
					#On obtient le texte de la zone de texte
					tampon = self.documentActif.zoneTexteDocument.get_buffer()
					debut = tampon.get_start_iter()
					fin = tampon.get_end_iter()
					texte = tampon.get_text(debut, fin, True)
					
					#On enregistre le fichier
					fichier.write(texte)
					fichier.close()
					#Il n'est plus à sauvegarder, donc on change le titre (plus d'étoile)
					self.documentActif.aSauvegarder = False
					self.definirTitre()
					return True
			else:
				if self.fenetre.get_title() == 'Nouveau document':
					#Si le document n'est pas modifié, mais que c'est un nouveau document, on peut l'enregistrer
					self.documentActif.aSauvegarder = True
					if not self.enregistrer(None):
						#Si l'utilisateur ne le sauvegarde pas
						self.documentActif.aSauvegarder = False
			self.definirTitre()
		else:
			self.afficherAvertissement("Il n'y a aucun document ouvert.")
	
	def enregistrerSous(self, widget):
		if self.documentActif != None: #Si un document est ouvert
			#On enregistre des informations temporaires au cas où l'utilisateur annulerait la sauvegarde			
			cheminTemp = self.documentActif.chemin
			aSauvegarderTemp = self.documentActif.aSauvegarder
			
			#On change le chemin pour '' et aSauvegarde pour True pour que l'enregistrement se fasse dans un autre fichier (enregistrer sous)
			self.documentActif.chemin = ''
			self.documentActif.aSauvegarder = True
			if not self.enregistrer(widget): #Si l'utilisateur annule l'enregistrement, on remet les informations d'avant
				self.documentActif.chemin = cheminTemp
				self.documentActif.aSauvegarder = aSauvegarderTemp
				self.definirTitre() #et on remet le titre (parce qu'on le change après l'enregistrement)
		else:
			self.afficherAvertissement("Il n'y a aucun document ouvert.")
	
	def quitter(self, widget, evenement = None):
		if self.documentActif != None:
			if not self.fermer(None) == False:
				#Si un document est ouvert, mais qu'il est fermé après, on quitter
				gtk.main_quit()
		else:
			#Si aucun document est ouvert, on quitte
			gtk.main_quit()
	
	def ouvrirFichier(self, nomFichier):
		#Cette méthode ressemble à la méthode self.nouveau()
		#Création d'un objet DocumentOuvert() avec chemin
		nouveau = DocumentOuvert()
		nouveau.chemin = nomFichier
		nouveau.ASauvegarder = False
		self.documents.append(nouveau)
		self.documentActif = nouveau
	
		nouveau.zoneTexteDocument = gtk.TextView()
		nouveau.zoneTexteDocument.show()
		self.barresDefilement.add(nouveau.zoneTexteDocument)
		tampon = nouveau.zoneTexteDocument.get_buffer()
		
		fichier = open(nomFichier, 'r')
		texteListe = fichier.readlines()
		texte = ''.join(texteListe)
		fichier.close()
		
		#Comme on doit changer le texte pour celui du fichier, il faut bien faire attention à connecter le signal 'changed' APRÈS de l'avoir changer pour pas que l'attribut aSauvegarder devienne True
		tampon.set_text(texte)
		tampon.connect("changed", self.fichierModifie)
		
		self.definirTitre()
	
	def ouvrirFichierDialogue(self, widget):
		continuer = False
		if self.documentActif != None:
			if not self.fermer(None) == False:
				#Si le document ouvert se ferme, on peut ouvrir la boîte de dialogue
				continuer = True
		else:
			#Si aucun document est ouvert, on peut ouvrir la boîte de dialogue
			continuer = True
		if continuer:
			ouvrirFichierDialogue = gtk.FileChooserDialog("Ouverture de fichiers", self.fenetre, gtk.FILE_CHOOSER_ACTION_OPEN, ((gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)))
			reponse = ouvrirFichierDialogue.run()
			if reponse == gtk.RESPONSE_OK:
				nomFichier = ouvrirFichierDialogue.get_filename()
				#On essaie d'ouvrir le fichier pour savoir s'il existe
				try:
					fichier = open(nomFichier)
				except:
					#Dans le cas où l'utilisateur aurait taper le nom du fichier et que celui-ci n'existerait pas, on affiche une erreur
					self.afficherErreur('Le fichier <b>' + os.path.basename(nomFichier) + '</b> n\'existe pas.', True)
				else:
					#S'il existe, on l'ouvre
					fichier.close()
					self.ouvrirFichier(nomFichier)
			ouvrirFichierDialogue.destroy()
		
	def afficherInformation(self, texte, use_markup = False):
		"Affiche une information (non utilisée)"
		if use_markup:
			#Si use_markup vaut True, on insère le texte avec set_markup()
			infoDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
			infoDialogue.set_markup(texte)
		else:
			infoDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, texte)
		infoDialogue.set_title("Information")
		infoDialogue.run()
		infoDialogue.destroy()
	
	def afficherAvertissement(self, texte, use_markup = False):
		"Affiche un avertissement"
		if use_markup:
			avertissementDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK)
			avertissementDialogue.set_markup(texte)
		else:
			avertissementDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, texte)
		avertissementDialogue.set_title("Avertissement")
		avertissementDialogue.run()
		avertissementDialogue.destroy()
	
	def afficherErreur(self, texte, use_markup = False):
		"Affiche une erreur"
		if use_markup:
			erreurDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK)
			erreurDialogue.set_markup(texte)
		else:
			erreurDialogue = gtk.MessageDialog(self.fenetre, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, texte)
		erreurDialogue.set_title("Erreur")
		erreurDialogue.run()
		erreurDialogue.destroy()
		erreurDialogue.destroy()
		
PyGtkApp()
gtk.main()


Voilà, vous savez maintenant comment faire un éditeur de texte assez simple. Après le chapitre 8, vous pourrez le terminer pour qu'il ait un panneau, des onglets et l'impression des fichiers.
Voilà terminé votre premier TP. J'espère que vous avez adoré. Je vous avais bien dit que vous étiez capables de faire de beaux programmes maintenant. Dans les prochains chapitres, vous continuerez votre apprentissage de widgets (vous ne croyez quand même pas que c'était fini ;) )
Chapitre précédent Sommaire Chapitre suivant

Partager

Il n'y a pas encore de commentaire pour ce tuto.

Ce tutoriel a été corrigé par les zCorrecteurs.