Ohjelmoinnin peruskurssi Y2, kurssimateriaali

Luku 3.5: Kaksiosainen tehtävä: Undo-Redo (100 + 200p)

Etusivulle

Luku 3.5: Kaksiosainen tehtävä: Undo-Redo (100 + 200p)

Tästä sivusta:

Mitä käsitellään? Tutustutaan Command-suunnittelumalliin.

Mitä tehdään? Toteutetaan ominaisuus jota voi soveltaa omaan projektiaiheeseen.

Pistearvo: 100+200p

Bonus: +5 % omista pisteistä aikaisesta palautuksesta.

Tehtävä palautetaan kahdessa osassa

  • Tehtävän ensimmäisessä osassa (100p) koodataan ja testataan luokka RemoveAction.
  • Tehtävän toisessa osassa (200p) koodataan ja testataan luokka DeleteTimeAction ** Huom!**: Toinen osa on huomattavasti työläämpi ja sen tekemisessä kannattaa miettiä pidempään ennen kuin aloittaa koodaamaan.

Yleisimmät ohiluetut kohdat tehtävänannossa

Näiden ymmärtäminen voi vaatia tehtävän lukemista ensin loppuun...

  1. Ei huomata että poistettavan alueen molemmat päät kuuluvat poistettavaan alueeseen.
  2. Metodille Split annettu ajanhetki kuuluu "poisleikattuun alueeseen", eli myös tuo aika poistetaan VideoSectionista jota leikataan.
  3. Luokalla TimeCode on käteviä metodeja.
  4. Metodia split ei koskaan pidä käyttää jollei sitä tarvita.

Intro

Suuressa osassa ohjelmia, joissa käsitellään jotakin dataa (kuvia, tekstiä, taulukoita) on mahdollista sekä tehdä dataan muutoksia, että perua muutoksia jälkeenpäin. Kutsumme tässä tätä toiminnallisuutta undo-redo -mekanismiksi.

Tapoja toteuttaa undo-redo on useita. Joissakin toteutuksissa tallennetaan kaikki muutettu data, joissakin vain muuttuneet osat. Meidän kannaltamme mielenkiintoista on kuinka tämän voi tehdä ohjelmassamme tyylikkäästi ja riippumatta siitä kuinka suuri muutos on tai minnepäin ohjelmamme tilaa se vaikuttaa.

Tyypillinen tapa toteuttaa Undo-Redo-mekanismi olio-pohjaisissa järjestelmissä perustuu Komento -suunnittelumalliin (Command design pattern). Tässä harjoituksessa rakennamme undo-redo mekanismin komento-suunnittelumallia käyttäen kuvitteelliseen videoeditointi-ohjelmaan.

Command Pattern - Komento-suunnittelumalli

Komento-mallissa ideana on ottaa jokin suoritettava asia ja "kääriä sen ympärille" olio. Komentojen ulkoinen rajapinta määritellään rajapinnan tai abstraktin luokan kautta. Tällöin komentoja suorittava osa ohjelmasta voi suorittaa ne tietämättä komentojen sisältöä. (Polymorfinen metodikutsu) Komento-mallilla voidaan toteuttaa mm.

  • Undo-redo mekanismi
  • Valikoiden toiminnallisuus käyttöliittymissä
  • Makrojen nauhoittaminen
  • jne...

Esimerkki

Yksinkertaisimmassa versiossa yliluokassa on vain yksittäinen metodi, jota kutsumalla jo aiemmin aseteltu toiminnallisuus saadaan aikaan. Esimerkissämme tämä metodi on nimeltään execute.

class Command(object):

    def execute()

Luokka DrawLineCommand on malliesimerkki luokasta joka täyttää em. yliluokan. Luokan ilmentymä saa parametrit ennalta määrättyä tehtäväänsä varten konstruktorin parametreina. Tämä ei kuitenkaan ole komento-suunnittelumallin vaatimus - olennaista on vain että olion tila asetetaan valmiiksi komennon suoritusta varten joka tapahtuu vasta kun execute-metodi suoritetaan.

class DrawLineCommand(Command):

    def __init__(self, draw_here, x1, y1, x2, y2):
        self.drawing_area = draw_here
        self.startX      = x1
        self.startY      = y1
        self.endX        = x2
        self.endY        = y2

    def execute(self):
        self.drawing_area.draw_line(x1, y1, x2, y2)

Lopuksi katsotaan kuinka komentoja luodaan ja suoritetaan. Tässä suoritetaan komentosarja joka piirtää täytetyn neliön. Huomaa viivanpiirtokomentojen joukossa oleva FillCommand, ja se ettei se muuta for-silmukkaa, jossa komentoja suoritetaan. Esimerkissämme arvot olivat kiinteitä, mutta todellisuudessa arvot komennolle voisivat tulla vaikka käyttäjän hiiren klikkailuista ja tällä tavoin saisi rakennettua vaikka makron jolla voi piirtää neliöitä.

# koodia
do_these_later.add(DrawLineCommand(this_pic, 0,   0, 15, 0) )
do_these_later.add(DrawLineCommand(this_pic, 15,  0, 15, 15) )
do_these_later.add(DrawLineCommand(this_pic, 15, 15,  0, 15) )
do_these_later.add(DrawLineCommand(this_pic, 0,  15,  0, 0) )
do_these_later.add(FillCommand(this_pic, 10,  10, color.RED) )

# koodia

# koodia
for this_command in do_these_later:
   this_command.execute()

# koodia

Komento-suunnittelumalli ja Undo-mekanismi

Komento-suunnittelumallin käyttö Undo-Redo mekanismin toteutuksessa lähtee siitä että yhden execute-metodin sijaan laitetaankin yliluokkaan kaksi "execute-metodia": Undo ja Redo.

Redo on käytännössä pitkälti toiminnaltaan sama kuin perusmallin execute-metodi. Undo-toimintoa varten joudutaan yleensä tallentamaan hieman enemmän informaatiota. Pystynet arvaamaan miten allaoleva koodi toimii.

class RenameCommand(Command):

    def __init__(self, person, new_name, old_name):
       self.person  = person
       self.newName = new_name
       self.oldName = old_name

    def undo(self):
        self.person.set_name(old_name)

    def redo(self):
        self.person.set_name(new_name)

Viimeinen osa mekanismia on komentojen käsittely. Komentoja varten on kaksi pinoa. Kun komento suoritetaan se laitetaan undo-pinoon. Kun tehdään undo, komento otetaan undo-pinosta, suoritetaan undo-metodi ja laitetaan se redo-pinoon. Redo-operaatio toimii päinvastoin.

Jos joku tekee jotakin uutta kun redo-pinossa on komentoja, ne täytyy yleensä heittää pois, koska ei voida taata edes että oliot joita komennot käsittelevät olisivat enää olemassa.

Tehtäväpohjan kuvaus

Tässä tehtävässä muokataan kuvitteellista videoeditoriohjelmaa. Editoriohjelmassamme on yksi videoraita (VideoTrack), joka sisältää videopätkiä (VideoSection). VideoTrack sisältää seuraavat metodit, joita kutsumalla raidan sisältöä voi muuttaa:

def add_section(self, section)
    # Lisää annetun videopätkän raidalle heti viimeisen pätkän perään.

def remove_section(self, section)
    # Poistaa annetun videopätkän raidalta. Tämä voi olla mikä tahansa raidalla oleva pätkä.
    # removeSection jättää raidalle ajallisesti tyhjän kohdan.

def delete_time(self, start, end)
    # Poistaa raidalta kaiken annettujen ajanhetkien väliltä.
    # Annetut alku ja loppuhetket sisältyvät poistettavaan alueeseen.

Vaatimukset toteutukselle

  1. Ei None:eja: VideoTrack:in sisältämä lista ei koskaan sisällä None-alkioita ja sen sisältämät videopätkät ovat aina kronologisessa järjestyksessä. Videopätkien väliin saa jäädä aikaa jolloin materiaalia ei ole. Toisinsanoen, videomateriaali ei liiku raidalla ajallisesti minnekään.
  2. Ei luoda explisiittisesti uusia olioita: Toimintojen on tarkoitus käsitellä vain jo olemassaolevia (esim testissä luotuja) ja VideoSection.split -metodin luomia olioita. Toisin sanoen: Luokista RemoveAction ja DeleteTimeAction ei ole tarkoitus kutsua VideoSection-luokan konstruktoria suoraan.

FAQ: Selvennystä ja vinkkejä tehtävää 2.3 koskien (30.1)

  • Tehtävässä 2.3 videopätkiä täytyy tarvittaessa jakaa komennolla split ja liittää yhteen komennolla join. Split-komennon antamat videopätkien nimet ovat sellaisenaan oikeat - itse asiassa sectioneiden nimiä ei voi edes muuttaa kuin tilanteessa jossa takaisin kokoon liitettävälle alueelle annetaan vanha nimi takaisin join-komennon yhteydessä.
  • Pohdi tarkkaan mistä kohdasta pätkät jaetaan tarvittaessa osiin - poistettavan alueen molempien päiden täytyy kuulua poistettuun alueeseen. Tutustu myös luokkaan TimeCode.

Esimerkki

Alkutila

Esimerkkimme alkutilassa videoraidalla (track) on kaksi pätkää.

  1. Näytös1 (00:00:00 - 00:02:39)
  2. Näytös2 (00:02:40 - 00:06:39)
../_images/without3.png

list : [naytos1, naytos2]

Lisätään videopätkä

Luodaanpa nyt uusi videopätkä nimeltään "Näytös3", jonka pituus on 00:02:20. Kun suoritetaan VideoTrack-luokan metodi add_section(example) ilman aikaparametria tulee uusi pätkä lisätyksi raidan loppuun. Tällöin lisätään myös lisäysoperaatiota kuvaava olio UndoManager-luokan peruttavien operaatioiden jonoon metodilla add_action(). (lisää tämä koodi kaikkiin operaatioihin)

# VideoSection example:
example = VideoSection("Näytös3", TimeCode(0,0,0), TimeCode(0,2,20))
track.add_section(example)
  1. Näytös1 (00:00:00 - 00:02:39)
  2. Näytös2 (00:02:40 - 00:06:39)
  3. Näytös3 (00:06:40 - 00:09:10)
../_images/original.png

list : [naytos1, naytos2, naytos3]

Undo

Jos nyt kutsutaan trackin undo-toimintoa, palataan alkutilaan:

track.undo()
../_images/without3.png

list : [naytos1, naytos2]

Redo

Jos nyt taas redo-metodia niin päästään tilaan jossa pätkiä on kolme.

track.redo()
../_images/original.png

list : [naytos1, naytos2, naytos3]

Poistetaan pätkä

Käytetään seuraavaksi trackin metodia delete_time. Lopputulos on tässä himpun verran monimutkaisempi kuin edellisissä tapauksissa, sillä jotta vain haluttu osuus poistettaisiin täytyy jotkin pätkät pilkkoa osiin. Tähän löytyy metodi split VideoSection luokasta, sekä vastakkainen metodi join, jolla pätkiä voi yhdistää. Split irroittaa halutusta pätkästä loppuosan ja lyhentää alkuperäistä pätkää. Loppuosa ei automaattisesti palaa VideoTrack:ille.

HUOM! Split-metodia tulee käyttää vain jos se todella jakaa pätkän kahteen osaan. Tarkista siis tilanne ennenkuin toimit.

Jos pilkotut loppuosat laittaisi takaisin VideoTrack:ille (kuten tehtävän vinkeissäkin neuvottiin) lopputulos näyttäisi tältä:

track.delete_time(TimeCode(0,1,20), TimeCode(0,7,0))
../_images/grayscale.png

Kun harmaalla merkitty alue poistuu näyttää lopputulos tältä:

../_images/timedelete.png

list : [naytos1-1, naytos3-2]

Undo

Undo palauttaa meidät jälleen tilaan jossa pätkiä on kolme. Huomaa että alueet ovat taas yhtenäisiä ja niillä on alkuperäiset nimet.

track.undo()
../_images/original.png

list : [naytos1, naytos2, naytos3]

Undo

Seuraava undo palauttaa meidät vielä pidemmälle.

track.undo()
../_images/without3.png

list : [naytos1, naytos2]

Tehtävänanto

Valmistautuminen

  1. Katso ensin luokkaa UndoManager. Huomaa kuinka UndoManager käsittelee UndoableAction luokan olioita vaikka se ei tiedä lopullisesta toteutuksesta mitään. Huomaa myös että Undomanager ja UndoableAction ovat täysin sovellusalueesta (videoeditori) riippumattomia luokkia, joten niitä voitaisiin soveltaa jossakin toisessa projektissa.
  2. Käy nyt katsomassa luokkia video.VideoTrack, video.VideoSection ja undo.AddAction. Tutki mitä tapahtuu kun VideoTrack-olion metodia add-section kutsutaan. Katso myös (vaikka debuggerilla) mitä tapahtuu kun kutsutaan metodeja undo ja redo.

Toteutus ja testaus

Toteutetaan ja testataan undo-redo kahdelle uudelle toiminnolle.

VideoTrack luokan metodit remove_section ja delete_time luovat ja käyttävät luokkien RemoveAction ja DeleteTimeAction toimintoja. Käy lukemassa niiden kuvaukset. Toteuta osatehtävässä 1 tämän perusteella luokka RemoveAction ja osatehtävässa 2 luokka DeleteTimeAction loppuun.

Huom!

  • Näiden toimintojen on tarkoitus käsitellä mahdollisuuksien rajoissa vain jo olemassaolevia ja VideoSection.split-metodin luomia olioita. Toisin sanoen: Luokista RemoveAction ja DeleteTimeAction ei ole tarkoitus kutsua VideoSection-luokan alustusmetodia suoraan.
  • Split-komennolla ei tule yrittää jakaa videopätkiä jos tarvetta jakamiseen ei ole. t.s. kohdassa on jo muutenkin kahden pätkän raja.

Testiluokan runkoon on jo toteutettu kaksi testiä malliksi. Ensimmäinen testaa onnistuiko videopätkän (VideoSection) lisääminen raidalle. Toinen testi testaa redo-toimintoa tilanteessa jossa yhtään uudelleensuoritettavaa komentoa ei pitäisi olla. Tarkoitus on lisätä joukko testejä, jotka testaavat kaikki koodin rivit ja suorittavat if/for/while jne lauseiden molemmat haarat. ts. jokaisen if-lauseen tulisi olla testatessa ainakin kerran tosi ja ainakin kerran epätosi.

Riittää, että testaat kattavasti vain luokan RemoveAction (osatehtävä 1) ja DeleteTimeAction (osatehtävä 2). Muuta ei tarvitse testata.

Heti kun olet luonut itsellesi Eclipseen projektin, voit ajaa pohjassa annetut testit. Molempien pitäisi mennä läpi.

Valmiiksi annettu koodi

Tässä tehtävässä on siinä määrin paljon koodia, että saat koodin zip-paketissa undoredo.zip.

Paketti joka sisältää tehtävässä tarvittavat luokat. Tämän paketin saat helposti Eclipseen luomalla ensin tyhjän PyDev-projektin, minkä jäkeen voit valita

File->Import->General->Archive file

Etsi sitten tämä paketti, aseta kansioksi luomasi projekti ja paina Finish.

Voit halutessasi purkaa ylläolevan paketin itse tai ladata tiedostot yksitellen. (alla)

Toimintaohjeet

Lue molemmat PyUnit-ohjeet, mikäli et vielä ole lukenut niitä. Halutessasi tee Eclipseen projekti jossa PyUnit:ia on helppo suorittaa.

Osatehtävän 1 palautus

Palauta A+:ssa luokat:

  • undo/remove_action.py
  • tests/test.py (Älä käytä DeleteTimeAction:a tässä testissä)

Osatehtävän 2 palautus

Palauta A+:ssa luokat:

  • undo/delete_time_action.py
  • tests/test.py (Älä käytä RemoveAction:ia tässä testissä)

Huom!

Muutoksia voi vielä tulla.

Etusivulle