6. Metin Düzenleyici

Birçok yeni programcı, programların çok basit şekilde hazırlanabileceğini düşünür. Oysa ki en küçük programda bile çok fazla düşlünülecek ve yapılacak iş vardır. Bu bölümde basit bir metin düzenleyici yapacağız. Elbette yapacağımız metin düzenleyici üretim açamlı olmayacaktır. Sadece bir program yazılırken, programcıların nelere dikkat etmeleri gerektiği, nereleri düşünmeleri gerektiğine bir ışık tutacağız. Basit bir metin düzenleyici yazmak için ne kadar çok yapılacak iş olduğunu göreceksiniz. Bir programı yazmaya başlayınca, düşünmenin sınırı ve yapılacakların sonu olmadığını göreceksiniz. Biz burada bir yol açalım, gerisini size bırakacağız.

6.1. Ana Pencere

Buradaki metin düzenleyici basitçe bir metin alanı ve kullanıcıya dosyasını açıp kaydedebileceği birkaç seçenek sunmaktan ibaret (tamamı bu değildir elbette) olacak. O halde öncelikle ana penceremizi hazırlayalım, bunun için şimdilik metin alanımızı ve altına işlem yapmayan birkaç düğme koyalım. Daha sonra bu düğmelere işlerlik kazandıracağız. main.py dosyasını Liste 6.1‘deki gibi yazalım.

Liste 6.1 main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# -*- coding: utf-8 -*-

import os, sys
from kivy.uix.label import Label
from kivy.app import App
from kivy.uix.popup import Popup

class metinDuzenleyici(App):

    def build(self):
        pass        
        
        
metinDuzenleyici().run()  

Bu programda bilmediğimiz hiçbirşey yok. os ve sys modüllerini neden içerdiğimizi ileride göreceksiniz. Şimdi de, bu program tarafından kullanılacak kv dosyasını Liste 6.2‘deki gibi yazalım:

Liste 6.2 metinduzenleyici.kv
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<metinDuzenleyici>:
BoxLayout:
    orientation: "vertical"
    exit_on_escape: False
    TextInput:
        size_hint_y: 90
        id: metin
        multiline: True

    BoxLayout:
        size_hint_y: 10
        Button:
            text: "Aç"
            on_press: pass
        Button:
            text: "Kaydet"
            on_press: pass
        Button:
            text: "Farklı Kaydet"
            on_press: pass
        Button:
            text: "Yeni"
            on_press: pass

Bu dosyada bilmediğimiz sadece TextInput parçacığının multiline özelliğine True değerinin atanmış olmasıdır. TextInput parçacığını

metin = TextInput(multiline=True)

şeklinde tanımlayacak olursak, çok satırlı bir metin girdi alanı elde etmiş oluruz. TextInput parçacığı ön tanımlı olarak tek satırdan oluşur, multiline özelliğine True değeri atamkla çok satırlı bir metin giriş alanı elde etmiş oluruz ki, bir metin düzenleyicinin metin yazılacak alanı tam da bu yaptığımız gibidir.

main.py programı çalıştırdığımızda metin düzenleyicimizin penceresi Sonucu Şekil 6.1 ‘deki gibi açılacaktır.

_images/metinDuzenleyici1Img.png

Şekil 6.1 Metin Düzenleyicimiz Kullanıma Hazır :-)

Şimdi yukarıdaki düğmelere tıklayın bakalım dosya açacak mı, dosyanızı kaydedecek mi? Sizce olur mu? Neden olmasın ki? Ben düğmeyi koydum, program da gitsin dosyamı kaydedeceğini anlasın ve kaydetsin. Benzer cümleleri her sınıfımda kullanırım. Ancak ne yazıkki bu programları yazanlar, bizim koyduğumuz düğmelerin (ya da menülerin) üzerindeki metinlere bakarak, ne iş yapmak istediğimizi anlamayacak kadar becereksizler. Öyle mi? Elbette değil. Bir düğme koyduysanız, ona tıklandığında ne yapılması gerektiğini siz yazacaksınız, bir başkası değil. Bu kadar söylendikten sonra gelin, bu düğmelere işlerlik kazandıralım. Öncelikle “Farklı Kaydet” düğmesinden başlayalım. Neden mi? Çünkü “Kaydet” dedğimizde, daha önceden bir dosya adı verilmemişse “Farklı Kaydet” çağrılacaktır.

6.2. Farklı Kaydet

Bir dosyayı kaydetmek için, öncelikle kaydedilecek klasörün belirtilmesi ve daha sonra da dosya adının girilmesi gerekir. Eğer kullanıcıya basitçe bir metin kurtusu sağlayıp buraya dosyanın tam patikasını yazmasını isterseniz (örneğin C:\dosyalarım\odevlerim\fizik\newton.txt şeklinde), kusura bakmayın ama programınızı kimse kullanmaz. 1990’lı yıllarda olsaydınız buna kimse itirtaz etmezdi ancak şimdi GUI (Grafik Kullanıcı Arayüzü) icat edildi. O halde klasörler arasında gezinti yapabilecek bir arayüze ihtiyacımız var. Bun kendiniz yapabilirsiniz. Nasıl mı? os.listdir()‘i biliyorsunuz. Her bir elemanı ızgara düzenine bir düğme olarak yerleştirip gezinti sağlayabilirsiniz. Ama bunu yapmayın. Çünkü Kivy geliştiricileri bizim için daha iyisini yapmış: FileChooserListView. Bu grafik parçacığı bize dizinler arası gezinti yapmamızı sağlayacak bir araç sunmaktadır. Bu parçacığın filters özelliği sayesinde, hangi dosyaları liseteleyeciğimizi de belirtebiliyoruz (aşağıda ['*.*'] kodu tüm dosyaların listeleneceğini göstermektedir. Üstelik bir seçim yaptığımızda on_selection olayı sayesinde, seçim ile ilgili işlemlerimizi yapabiliyoruz. O halde bu parçacığı en üste koyalım, altına bir adet dosya adının yazılabileceği metin kutusu, onun altına da iki adet düğme: “Kaydet”, “Vazgeç”. O halde metin düzenleyicimizin pencerelerinin oluşturulduğu metinduzenleyici.kv dosyasına aşağıdaki gibi yeni bir form ekleyelim:

Liste 6.3 FakrliKaydetForm
 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
<farkliKaydetForm>:
    title: "Dosya Kaydet"
    size_hint: (.9, .9)
    BoxLayout:
        orientation: 'vertical'

        FileChooserListView:
            size_hint_y: 80
            id: dosya_secim
            filters: ['*.*']
            path: app.son_patika
            on_selection: pass

        BoxLayout:
            size_hint_y: 10
            Label:
                text: "Dosya Adı:"
                size_hint_x: 20
            TextInput:
                id: dosya_adi
                size_hint_x: 80

        BoxLayout:
            size_hint_y: 10
            Button:
                text: "Kaydet"
                on_press: pass
            Button:
                text: "Vazgeç"
                on_press: root.dismiss()

Bu formu kullanacak bir sınıf tanımlamak gerekiyor. Bunu class metinDuzenleyici(App) satırından önce aşağıdaki kodları ekleyerek yapabiliriz:

class farkliKaydetForm(Popup):
    pass

Dosya ismini ve kaydedildiği patikayı takip etmemiz gerekmektedir. Nedenini şöyle açıklayabiliriz, eğer bir dosya açılmış (ya da bir isimle kaydedilmiş) ise, dosya ismi var olacağından tekrar kaydedilmesi sırasında bı dosya ismini kullanmamız gerekmektedir. Dosya ismi gerekli ise, kaydedildiği klasör de gereklidir. O halde tüm programımız içerisinde dosya ismi ve patikayı kullanabileceğimizden, build() işlevine aşağıdaki satırları eklememiz gerekecek.

self.son_patika= os.getcwd()
self.son_dosya=''

Bu satırları ekledikten sonra build() işlevindeki pass ifadesine gerek kalmayacak.

Sanırım main.py programının başında os modülünü çağırma nedenimizi anladınız. Burada son patika ön tanımlı olarak programın çalıştığı patikayı göstermektedir, son dosya ise boş bir cümledir, yani dosya adı yoktur.

Ana pencerede “Farklı Kaydet” düğmesine tıklandığında farkliKaydetForm‘nun açılabilmesi için, üzerinde “Farklı Kaydet” yazan düğmeye tıklandığında bir işlevin çağrılması ve bu işlev altından da bu formu gösterebilmemiz gerekiyor. O halde bu düğmenin on_press özelliğini farkliKaydetDialog() işlevini çağıracak şekilde aşağıdaki gibi değiştirelim (Liste 6.2‘deki 18, 19 ve 20. satırlar):

Button:
    text: "Farklı Kaydet"
    on_press: app.farkliKaydetDialog()

İşimiz henüz bitmedi, çünükü düğmeye tıklandığında çağrılacak olan işlevi yazmadık, bunun için build() işlevinden hemen önce aşağıdaki işlevi tanımlayalım:

def farkliKaydetDialog(self):
    form = farkliKaydetForm()
    form.open()

Artık “Farklı Kaydet” düğmesine tıkladığımzda yeni bir pencere açılmaktadır. Açılacak olan pencereyi Şekil 6.2 ‘de görüyorsunuz:

_images/metinDuzenleyiciFarkliKaydetImg.png

Şekil 6.2 Farklı Kaydet Penceresi

Peki bu pencere şu anda ne iş yapar? Sadece dosya ve klasörleri listeler (bunuda biz yapmadık FakrliKaydetForm‘de 7. satırda tanımladığımız FileChooserListView parçacığı yapıyor). Haa birde çok büyük bir iş olan “Vazgeç” düğmesine tıklandığında pencere kapanıyor. Sanırım bunu bizden başka başarabilecek kimse de yok :-). Peki bu nasıl gerçekleşiyor? Pencerenin kapanmasını bizim yazdığımız kod sağlamkta, şaka yapmıyorum :-) Peki nersi? Yanılmıyorsam (yanılıyorsam lütfen beni uyarın!), FakrliKaydetForm‘deki 30. satır bu işi yapıyor olmalı.

Az zamanda çok işler başardığımızı söylemek isterdim, ancak gerçek bu değil. Daha henüz işe yarar birşey yapmadık. Öncelikle FileChooserListView parçacığının gösterdiği dosya isimleri üzerine tıklandığında (diğer bir deyişle var olan dosya ismini seçip bu dosya üzerine kaydetmek isteyebilir, aman dikkat !! çok tehlikeli, kaydetmeden önce onay almalısınız, bunu size bırakıyoruz) bu dosya isminin dosya adı yazılacak olan (id‘si dosya_adi olan TextInput parçacığı) metin kutusunda belirmelidir. Bunu nasıl yapacağız? Daha önce demiştik ya on_selection özelliği ile. Bu özelliği dosya seçimi işleminden sonra çağrılacak olan işleve bağlayabiliriz. Bunun için FileChooserListView parçacığının on_selection özelliğini aşağıdaki gibi değiştirin (FakrliKaydetForm‘de 12. satırı aşağıdaki gibi değiştirin):

on_selection: app.farkliKaydetSecim(root)

Burada ne yapılıyor? Her sonunun cevabını verdik te bunun cevabını mı esirgeyelim? Burada yapılan iş, formun kendisini uygulamanın farkliKaydetSecim() işlevine argüman olarak göndermek. Gerisini bu işlev halletmektedir. O zaman bu işelvi yazmalıyız. build()‘den hemen önce aşağıdaki işlevi tanımlayalım:

def farkliKaydetSecim(self, form):
    secilen_dosya=form.ids.dosya_secim.selection
    if secilen_dosya:
        if len(secilen_dosya)>0:
            dosyaAdi=os.path.split(secilen_dosya[0])[1]
            form.ids.dosya_adi.text=dosyaAdi

Bu işleve farkliKaydetForm‘nun kendisi argüman olarak geliyordu, bunu form değişkenine aktardık. FileChooserListView nesneninden seçilen dosyayı selection özelliği ile alabaliriz. Bu bize seçilen tüm dosyaları (çoklu seçim yapılabilir, multiselect özelliğinin değeri True yapılarak). Ön tanımlı olarark sadece bir dosya seçilebildiğinden, ilk dosyayı patika ve dosya adı olarak ayırdıktan sonra yaptığımız iş dosya adını formumuzda ids’si dosya_adi olan metin kutusunda göstermektir.

Şimdi gelelim “Kaydet” düğmesine tıklandığında yapılacak olan işe: formadki metin kutusundan dosya adını alacak ve diske yazma işlemi gerçekleştirilecek. Diske yazma işlemini farklı bir işlevde yapacağız, çünkü ana pencerede “Kaydet” düğmesine tıklandığında da bu işlevi çağıracağız. Önce “Kaydet” düğmesine tıklandığında çağrılacak olan işlevi belirtmeliyiz, ardındanda bu pencereyi kapatmalıyız. O halde FakrliKaydetForm deki 27. satırı şu şekilde değiştirelim:

on_press: app.farkiKaydetIslevi(root); root.dismiss()

Gelelim farkiKaydetIslevi() işlevine: bu işlevi build() den hemen önce şu şekilde tanımlayabiliriz:

def farkiKaydetIslevi(self, form):
    self.son_patika=form.ids.dosya_secim.path
    self.son_dosya=form.ids.dosya_adi.text
    self.dosyaKaydet()

Bu basit işlevde, formda dosyanın patikası alınıp self.son_patika değişkenine aktarılıyor. Benzer şekilde self.son_dosya değişkenine formdaki ids’si dosya_adi olan metin kutusundaki dosya adı aktarılıyor. Son olarak dosyaKaydet() işlevi çağrılıyor. Şimdi de bu işlevi yazalım (build() den hemen önce):

def dosyaKaydet(self):
    if not self.son_dosya:
        self.hataGoster("Dosya adı verdiğinizden eminmisiniz?")
    else:
        try:
            dosya_tam_isim = os.path.join(self.son_patika, self.son_dosya)
            F=open(dosya_tam_isim, 'w')
            F.write(self.root.ids.metin.text)
            F.close()
        except:
            self.hataGoster("Dosyayı yazamadım. Nedeni: [color=#FF0000]%s[/color]"
                             % str(sys.exc_info()[1][1]))

Tahmin ettiğinizden daha karmaşık değilmi? Bir defa hataGoster() diye bir işlevin tanımlanması gerekiyor. Eğer kullanıcı dosya adı yamamış ise (formdaki ids’si dosya_adi olan metin kutusu boş ise), kullanıcıya popup pencere ile bunu bildirmeliyiz. Bunu hataGoster() işlevini tanımlayarak yapabiliriz. Eğer dosya ismi var ise, yazma işlemi gerçekleştirilecek. Bu ise biraz tehlikeli bir iş, öncelikle dosyanın tam adı oluşturuluyor (self.son_patika ve self.son_dosya değişkenlerinin değerleri os.path.join() ile). Son olarak dosya açılıyor ve üzerine yazılıyor. Bunu try: bloğunda yaptık, çünkü dosya bir sebepten dolayı yazılamayabilir. Nasıl bir sebep olabilir ki? Şöyle: disk dolu olabilir, dosyaya yazma yetkisi olmayabilir, klasöre yazma yetkisi olmayabilir, böyle bir disk olmayabilir .... Her ne sebeptenolursa olsun, dosya yazılamadığında programın kırılmayacak ve except: bloğu işletilerek dosyanın yazılamama sebebi kullanıcıla iletilecek. Bu da yine hataGoster() işlevi çağrılarak yapılıyor. Peki bu hataGoster() işlevi kimin nesi? Kimsenin birşeyi değil! Sadece aşağıdaki kodlardan oluşan gariban bir işlev:

def hataGoster(self, hata):
    icerik=Label(text=hata, markup=True)
    popup = Popup(title='Yapamadım !', content=icerik)
    popup.size_hint = (0.7,0.7)
    icerik.bind(on_touch_down=popup.dismiss)
    popup.open()

Bu işlevi build() den hemen önce tanımlayabilirsiniz.

Aslında düzenlenen bir dosyayı bu şekilde doğrudan yazmak akıllı bir programcını yapacağı iş değildir. Eğer bir nedenden dolayı dosya yazılamaz ise, program sonlanır ve kullanıcının önceki yeazdıkları da dahil olmak üzere kaybolur. Bu şekilde kaydedilirken bir problem çıkması durumunda çoğu zaman boş bir dosya elde edilir. Olası durumları kontrol ettik, ancak birde kontrol edemediğimiz durumlar var. Öreğin tam yazma aşamasında, elektrik kesilirse! Bu durumda kullanıcıya boş bir dosya verir bol küfür alırsınız. Tüm programlama dillerinde diske yazma işlemi doğrudan gerçekleşmez, belirli bir buffer büyüklüğü vardır bu dolduğunda diske yazılır daha sonra buffer’in tekrar dolması beklenir (hızdan tasaffur, disk kullanımından tasarruf gibi nedenlerle). Bu nedenle dosya kapatılana kadar (close()) yazma işleminden emin olmazsınız [her harfi yazdıktan sonra flush() kullanmamışsanız :-)]. En iyisi önce dosyayı geçici olarak yazmak, daha sonra dosya adını değiştirmektir. Örneğin:

F=open(dosya_tam_isim+'~', 'w')
F.write(self.root.ids.metin.text)
F.close()
os.rename(dosya_tam_isim+'~', dosya_tam_isim)

Burada da yine dikkat etmemiz gereken şey, dosya_tam_isim dosyasının yazılabilir olduğundan emin olmaktır. Bunuda

os.access(dosya_tam_isim, os.W_OK)

ile kontrol edebilirsiniz. “Farklı Kaydet” bitti. Şimdi sıra geldi “Kaydet”e

6.3. Kaydet

Bu sandığınızdan daha kolay. Çünkü birçok işi, “Farklı Kaydet”de yaptık. Öncelikle ana pencerede “Kaydet” düğmesinde tıklandığında bir işlev çağırmalıyız ve tüm işi bu işelve yaptırtmalıyız. Bunun için Liste 6.2‘deki 17. satırı aşağıdaki gibi değiştirelim:

on_press: app.dosyaKaydetIslevi()

Şimdi de bu işlevi yazalım, build() den hemen önce şu şekilde tanımlayabiliriz:

def dosyaKaydetIslevi(self):
    if self.son_dosya: self.dosyaKaydet()
    else: self.farkliKaydetDialog()

Burada dikkat ederseniz, öncelikle dosya adının olup olmadığına bakılıyor. Dosya adı var ise doğrudak dosyaKaydet() işlevi çağrılıyor. Bunu daha önce yazmıştık. Dosya adı yok ise, farkliKaydetDialog() işlevi çağrılıyor. Bunu da “Farklı Kaydet” kesitinde yazmıştık. Çok kolaymış değil mi?

6.4. Aç

Var olan bir dosyayı açmak için kullanacağımız bu düğme, eğer dikkatli olmazsak başımıza iş açabilir. Sebep? Eğer düzenlenmekte olan bir dosya kaydedilmeden, başka bir dosya açılmaya kalkışılırsa ve siz bunu kullanıcya bildirmemişseniz, bu durumda istenmedik laflar işitebilirsiniz (merak etmeyin, nasıl olas programı kullanan uzakta olacağından duymazsınız). İşitmeseniz bile buna dikkat etmek iyi bir programcı olduğunuzu gösterir. Peki bir metnin değiştiğini ve kaydedildiğini nasıl anlayacağız? Bunu bizim için yapacak bir kimse yok. Bu nedenle başımızın çaresine bakmalıyız.

Önce metnin değişip değişmediğini bilmemiz gerekiyor, bunu tanımlayacağımız self.metin_degisti değişkeni ile takip edebiliriz. O halde build() işlevi altına aşağıdaki satırları ekleyelim:

self.metin_degisti=False

Değerini False yaptık çünkü başlangıçta bir netnin içeriği değişmemiştir. Metin değiştikçe bunun değerini True, kaydettikçe değerini False yapmalıyız. Önce kaydettiğimizde değerin False olması için dosyaKaydet() işlevindeki F.close() satırından hemen sonra şu satırı eklemeliyiz:

self.metin_degisti=False

Bu tamam, peki metnin değiştiğini nasıl anlayacağız? Bunu bize Kivy söyleyebilir. TextInput perçacığının text‘ine bir işlev bağlarsak, metin değiştikçe bu işlev çağrılır. O halde build() işlevinin altına aşağıdaki satırları eklemeliyiz:

self.root.ids.metin.bind(text=self.metinDegisti)
self.ilkAcilis=True

Buradaki self.ilkAcilis değişkeni, programın ilk açlıp açılmadığını takip etmek için gereklidir. Çünkü TextInput nesnesi oluşturulur oluşturulmaz self.metinDegisti işlevi çağrılır. Buda programın ilk açılıp açılmadığına göre self.metin_degisti değişkeninin değerini değiştirmelidir. Bize gerekli olan metinDegisti() işlevini build() den hemen önce şöyle tanımlayabiliriz:

def metinDegisti(self, nesne, deger):
    if self.ilkAcilis: self.ilkAcilis=False
    else: self.metin_degisti=True

Eğer programımız ilk açılışta bu işlev çağrılıyorsa, if self.ilkAcilis değişkeninin değeri False yapılıyor, sonraki çağrılışlarda (metin girişi yapılır veya dosya okunursa), self.metin_degisti değişkeninin değeri True yapılıyor.

Şu ana kadar dosya açma ile ilgili birşey yapmadık, sadece metnin değişip değişmediğini takip ettik. Öncelikle dosya açılma işlemini tıpkı “Kaydet”de olduğu gibi, bir dizin tarayıcı oluşturmamız gerekiyor. Bunu yine FileChooserListView ile yapabiliriz. Bunun için bir form ve bu formu oluşturacak kv kodlarına ihtiyacımız var. metinduzenleyici.kv dosyasına aşağıdaki kodu ekleyin:

Liste 6.4 dosyaAcForm
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<dosyaAcForm>:
    title: "Dosya Aç"
    size_hint: (.9, .9)
    BoxLayout:
        orientation: 'vertical'
        
        FileChooserListView:
            size_hint_y: 90
            id: dosya_secim
            filters: ['*.*']
            path: app.son_patika
    
        BoxLayout:
            size_hint_y: 10            
            Button:
                text: "Aç"
                on_press: app.dosyaOku(dosya_secim); root.dismiss()
            Button:
                text: "Vazgeç"
                on_press: root.dismiss()

Bu kv formunu kullanacak sınıfı tanımlamak gerekiyor. Bunu class metinDuzenleyici(App) satırından önce aşağıdaki kodları ekleyerek yapabiliriz:

class dosyaAcForm(Popup):
    pass

Şimdide “Aç” düğmesine tıklandığında çağrılacak olan işlevi kv dosyasında belirtelim. Bunun için Liste 6.2‘deki 14. satırı aşağıdaki satırı şöyle değiştirelim:

on_press: app.dosyaAcIsleviDialog()

Şimdi de bu işlevi tanımlamak gerekiyor. build() den hemen önce işlevimizi şöyle tanımlayabiliriz:

def dosyaAcIsleviDialog(self):
    if self.metin_degisti:
        self.hataGoster("Dosya kaydedilmedi. Önce kaydedin.")
    else:
        self.dosyaAcDialog()

Bu işlev anladığınız üzere, dosyanın değişip değişmediğini kontrol ediyor. Eğer kaydedilmemişse, kaydetmesi için uyarıyor. Kaydedilmiş ise, dosyaAcDialog() işlevini çağırıyor. O halde bu işevi de build() den hemen önce şu şekilde tanımlayabiliriz:

def dosyaAcDialog(self):
    form = dosyaAcForm()
    form.open()

“Dosya Aç” formu açıldığında allta iki adet düğmemiz olacak. “Vazgeç” düğmesine tıklandığında form kapanacak. “Aç” düğmesine tıklandığında ise, dosyaOku() işlevi çağrılıyor (metinduzenleyici.kv dosyasına eklediğimiz Liste 6.4‘deki 17. satır). Bu işlev oldukça basit, sadece seçilen dosyayı gidip okuması gerekiyor. Bunu da build() den hemen önce aşağıdaki gibi tanımlayabiliriz:

def dosyaOku(self, dosya_secim):
   if dosya_secim.selection:
        if len(dosya_secim.selection)>0:
            (self.son_patika,self.son_dosya)=os.path.split(dosya_secim.selection[0])
            try:
                self.root.ids.metin.text=open(dosya_secim.selection[0]).read()
                self.root.ids.metin.cursor=self.root.ids.metin.get_cursor_from_index(0)
                self.metin_degisti=False
            except:
                self.hataGoster("Dosyayı Okuyamadım. Nedeni: [color=#FF0000]%s[/color]"
                             % str(sys.exc_info()[1][1]))
   else:
        self.hataGoster("Dosya seçtiğinizden eminmisiniz?")

Bu işlevdeki hemen herşeyi daha önce anlattık. Şimdi bir sorunumuz var (bitti mi ki?). Kullanıcı dosyayı düzenleyip yeni dosya açmak istediğinde sadece “Dosya kaydedilmedi. Önce kaydedin.” uyarısında bulunuyor. Oysa ki iyi bir program dosyayı açmadan önce dosyanın değiştiğini uyarmalı ve kullanıcıya kaydedip kaydetmeyeceği ile ilgili seçenek sunmalıdır. Bunun için yeni bir form tasarlamalıyız. Bu form sedece mevcut dosyanın kaydedilip kaydededilmemesini veya dosya açma işleminden vazgeçilmesini önermelidir. Böyle bir formu metinduzenleyici.kv dosyasına aşağıdaki satırları ekleyerek tasarlayabiliriz:

Liste 6.5 dosyaKaydedilmediForm
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<dosyaKaydedilmediForm>:
    title: 'Mevcut Dosya Kaydedilmedi'
    size_hint: (.9, .9)
    BoxLayout:
        orientation: 'vertical'

        Label:
            size_hint_y: 90  
            text: "Çalıştığınız dosya kaydedilmedi. Kayıt yapılsın mı?"        

        BoxLayout:
            size_hint_y: 10                
            Button:
                text: "Kaydet"
                on_press: app.dosyaKayedilmediKaydet(root)
            Button:
                text: "Hayır"
                on_press: app.dosyaAcDialog(); root.dismiss()
            Button:
                text: "Vazgeç"
                on_press: root.dismiss()

Bu kv formunu kullanacak sınıfımızı class metinDuzenleyici(App) satırından önce aşağıdaki kodları ekleyerek yazabiliriz:

class dosyaKaydedilmediForm(Popup):
    pass

Peki bu form’u nerede çağıracağız? Bildiniz değilmi? Yoksa bilemediniz mi? Bilemeyenlere hemen söyleyeyim, dosyaAcIsleviDialog() işlevinde dosya açmak istediğinde “Dosya kaydedilmedi. Önce kaydedin.” uyarısında bulunuyorduk ya işte burada. Yani bu uyarının yapıldığı satırın yerine yazmalıyız. O halde dosyaAcIsleviDialog() işlevini aşağıdaki gibi değiştirmeliyiz:

def dosyaAcIsleviDialog(self):
    if self.metin_degisti:
        kaydedilmedi_form = dosyaKaydedilmediForm()
        kaydedilmedi_form.open()
    else:
        self.dosyaAcDialog()

Dosyanın kaydedilmediği durumda dosya açmaya kalkışıldığında kullandığımız form’da kullanıcı “Kaydet” düğmesine tıklarsa dosyamızın kaydedilmesi için dosyaKayedilmediKaydet() işlevi çağrılmaktadır (Liste 6.5‘deki 15. satır). Bu işlevi build() den önce şu şekilde yazabiliriz:

def dosyaKayedilmediKaydet(self, kok):
    if self.son_dosya:
        self.dosyaKaydet()
        kok.dismiss()
        self.dosyaAcDialog()
    else:
        kok.dismiss()
        self.hataGoster("Dosya adı yok. 'Farklı Kaydet' kullanarak kaydetmelisiniz.")

Burada benimde hoşalşamadığım ve birçok kullanıcı için de garip gelecek bir durum var. Eğer dosya daha önce kaydedilmemiş ise, dosya adı (self.son_dosya değişkeninde saklanan) olmayacaktır ve bu durumda kullanıcıya “Farklı Kaydet”i kullanarak kaydetmesi önerisi sunulmaktadır. Oysa ki bunun yerine doğrudan farklı kaydet dialoğu (farkliKaydetDialog()) çağrılmalıydı. Bu Kivy’de olmadı. Bunu yapabilmemiz için, bu diyalog açıldıktan sonra, programın kullanıcıdan tepki gelene kadar hiçbir iş yapmaması gerekir (diğer bir deyişle program akışı durdurulmalıdır). Kivy’de ne yazıkki bu yok, en azından ben bilmiyorum.

6.5. Yeni

Kullanıcı bir dosya üzerinde çalışırken yeni bir dosya açmak isteyebilir. Bunun için ana penceremizin sağ alt tarafya bunulnan “Yeni” düğmesine tıklayacak. Şimdi bunun üzerinde çalışalım. Yapacağımız işi şöyle özetleyebiliriz: ilk olarak mevcut dosya değiştirilmiş ve henüz kaydedilmemiş ise, bunu kullanıcıya bildirmemiz gerekir, daha sonra yeni dosya oluşturma işlemine geçeceğiz. Öncelikle ana penceredeki “Yeni” düğmesine tıklandığında çağrılacak olan işlevi belirtmek için metinduzenleyici.kv dosyasındaki (Liste 6.2) 23. satırı şu şekilde değiştirmemiz gerekmektedir:

on_press: app.yeniDosyaAcIslevi()

Bu işlevi de build() den önce şu şekilde yazabiliriz:

def yeniDosyaAcIslevi(self):
    if self.metin_degisti:
        form = yeniDosyaForm()
        form.open()
    else:
        self.yeniDosyaAc()

Burada metnin değişmesi durumumnda yeni bir dialog (form) açılacak. Bu forma ait kv kodlarını metinduzenleyici.kv dosyasına aşağıdaki satırları ekleyerek oluşturabiliriz:

Liste 6.6 yeniDosyaForm
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<yeniDosyaForm>:
    title: 'Mevcut Dosya Kaydedilmedi'
    size_hint: (.8, .8)
    BoxLayout:
        orientation: 'vertical'
        Label:
            size_hint_y: 90  
            text: "Çalıştığınız dosya kaydedilmedi. Yeni dosya açmak istediğinizden emin misiniz?"        
        BoxLayout:
            size_hint_y: 10                
            Button:
                text: "Vazgeç"
                on_press: root.dismiss()
            Button:
                text: "Evet"
                on_press: app.yeniDosyaAc(); root.dismiss()

Bu kv kodlarını kullanacak olan yeniDosyaForm() sınıfınıda class metinDuzenleyici(App) satırından önce aşağıdaki kodları ekleyerek yazabiliriz:

class yeniDosyaForm(Popup):
    pass

yeniDosyaForm’unda kullanıcı yeni dosya açmaktan vazgeçerse zaten form kapanıyor, eğer kaydetmek için “Evet” düğmesine tıklarsa, yeniDosyaAc() işlevi çağrılıyor. Yeni dosya açma işlevini build() den hemen önce şöyle yazabiliriz.

def yeniDosyaAc(self):
    self.root.ids.metin.text=""
    self.son_dosya=""

En kolayı bu oldu sanırım, self.son_dosya değişkeninin değeri ile metin alanının değerini boş cümle yaparak yeni dosyayı oluşturmuş olduk.

6.6. Çıkmadan Önce

Henüz bitmedi. Çıkmadan önce yapılacak işlerimiz var. Kullanıcı metni düzenlerken çıkmak isterse ne yapacağız? Öncelikle, maobil cihazın “Geri” tuşuna basarak programdan çıkması engellenmeli ve çıkış kontrollü bir şekilde yapılmalıdır. “Geri” tuşuna basarak çıkmayı engellemek için programın başında bunu yapmak gerekiyor, yani daha uygulamayı başlatmadan önce. Geri tuşu ile çıkışı engellemek için main.py programının ikinci ve üçüncü satırına aşağıdaki kodları yazabilirsiniz:

from kivy.config import Config
Config.set('kivy', 'exit_on_escape', 'False')

aslında Config modülü daha fazla iş yapabilmektedir. Burada sadece geri tuşu ile çıkmayı engellemek için kullandık, bunu exit_on_escape parametresini False yaparak gerçekleştirmiş olduk. Windows ya da Linux’da pencere kapatma düğmesi ile hala programdan çıkılıyor olmalı, bunu dikkate almayın çünkü nasıl olsa programımız mobil cihazlarda çalışacak. Geri tuşu ile çıkmayı engelledik te, kullanıcı nasıl çıkacak? İsterseniz ana penceremizin sağ alt köşesine küçük bir düğme koyalım ve bu düğmeye tıklandığında çıkışı gerçekleştirelim. Böylelikle çıkmak isteyen kullanıcı bu tuşa basacak ve bir işlev çağrılacaktır. Bu işlevde istediğimizi kontrol edebiliriz. Çıkış düğmesini eklemek için metinduzenleyici.kv dosyasının (Liste 6.2) ana pencere düzenini oluşturan metinDuzenleyici formunun altındaki düğmeleri oluşturan BoxLayout altına aşağıdaki gibi bir düğme ekleyelim:

Button:
    id: cik_dugmesi
    size_hint_x: .15
    background_color: (0, 1, 0, 1)
    on_press: app.cik()

Dikkat etmişseniz, oldukça küçük bir düğme (%15 boyutunda) ve arka plan rengi yeşil olarak görünecek. Bir düğmenin arka plan rengi ni background_color özelliği ile ayarlayabiliyoruz. Bu özellik, diğer Kivy renk tanımlarında da kullanılabileceği gibi, bir tüp (isterseniz bir liste) alır. Bu tüpün 4 elemanı olacaktır. Bu tüp ile rengi şöyle belirliyoruz:

(R, G, B, T)

Buradaki harfleri anlamları şöyledir:

R: Kırmızı, G: Yeşil, B:Mavi

Renk oranlarını belirtmektedir. değerleri 0 ile 1 arasındadır. Bildiğimiz standart RGB ile aynı ancak 1 sayısı 255’e karşılık gelmektedir. En sondaki T Saydamlığı belirtmektedir. Bu değere 1 girerseniz tam katı (kesif, opak), 0 girerseniz tam saydam olur.

Tekrar dönelim düğmemize, akrka plan rengini neden yeşil yaptık? Çünkü yeşil doğa ve orman rengi değil mi? :-) Elbette bunun için değil, düğme yeşil olduğunda çıkış serbest olacak, kırmızı olduğunda metin değiştirilmiş fakat kaydedilmemiş olacak. O halde programımız içerisinde

self.metin_degisti=False

satırının olduğu her yerde aşağıdaki satırı ekleyerek düğmeinin yeşil renkli olmasını sağlayacağız:

self.root.ids.cik_dugmesi.background_color = [0, 1, 0, 1]

Peki ne zaman kırmızı yapacağız? self.metin_degisti değişkeninin değerinin True olduğu yerlerde. Bunu da programımız içerisindeki

self.metin_degisti=True

satırının olduğu her yerde aşağıdaki satırı da eklemeliyiz:

self.root.ids.cik_dugmesi.background_color = [1, 0, 0, 1]

Bu satırları yazmayı ben başarabildim, eminim (aslında Mustafa’yım da sözün gelişi) sizde yapabileceksiniz. Çık düğmesinin yeşile dönmesi gereken bir yer daha kaldı: yeniDosyaAc() işlevi. Bu işlevin en sonuna da aşağıdaki satırı eklemeliyiz:

self.root.ids.cik_dugmesi.background_color = [0, 1, 0, 1]

Şimdi programınızı çalıştırın ve düğmenin rengini takip edi. Program açılışta yeşil renki çık düğmesi ile başlayacak. Ne zaman metin yazarsanız, renk kırmızıya dönecek. Metni kaydettiğinizde tekrar yeşile dönecek. Ancak henüz çık düğmesi işe yaramıyor çünkü düğmeye tıklandığında çağrılacak olan app.cik() işlevini yamadık. Önce bu düğmenin nasıl davranacağını düşleyelim. Bir defa metin değişmiş ise, programdan çıkmadan önce kaydedilip kaydedilmeyeceğini sormalı. O zaman bir tane form oluşturmalıyız ve bunu sormalıyız. metinduzenleyici.kv dosyasına aşağıdaki gibi bir form ekleyelim:

Liste 6.7 cikmadanOnceForm
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<cikmadanOnceForm>:
    title: 'Mevcut Dosya Kaydedilmedi'
    size_hint: (.8, .8)
    BoxLayout:
        orientation: 'vertical'

        Label:
            size_hint_y: 90  
            text: "Çalıştığınız dosya kaydedilmedi. Çıkmak istediğinizden emin misiniz?"        

        BoxLayout:
            size_hint_y: 10                
            Button:
                text: "Vazgeç"
                on_press: root.dismiss()
            Button:
                text: "Yinede Çık"
                on_press: app.stop()

Bu form’da bilmediğiömiz tek şey stop() işlevidir. Bu işlev uygulamadan çıkma işlemini gerçekleştirir. Bu formu kullanacak sınıfımızı da class metinDuzenleyici(App) satırından önce aşağıdaki kodları ekleyerek yazabiliriz:

class cikmadanOnceForm(Popup):
    pass

Son olarak cik() işlevini yazalım. build()‘den hemen önce aşağıdaki satırları yazalım:

def cik(self):
    if self.metin_degisti:
        kaydedilmedi_form = cikmadanOnceForm()
        kaydedilmedi_form.open()
    else:
        self.stop()

Programımız artık temel ihtiyaçları karşılayacak düzeye geldi. Peki bitti mi? Haaayııır. Neler kaldı? Hayal etmenin sınırı yok. Örneğin son açılan dosyaların listesi, program açıldığında en çalışılan dosyanın otomatik açılması, kelime bulma ve değiştirme ... Başka ? Bir de kahve yapsın, yemek istemiyoruz :-)

Anlattıklarımızı takip edemediyseniz, yada ben yaptıklarımı gözden kaçırıp eksik yazmışsam, bu bölümde anlattıklarımı yaptığım dosyaları şu adreslerden alabilirsiniz:

main.py: https://github.com/mbaser/kivy-tr/blob/master/docs/programlar/metinDuzenleyici/6/main.py

metinduzenleyici.kv: https://github.com/mbaser/kivy-tr/blob/master/docs/programlar/metinDuzenleyici/6/metinduzenleyici.kv