Kumottavat toimet

Edellä on tarkasteltu monia tapoja, joilla lisätään toimintoja työympäristöön, mutta vielä ei ole käsitelty toiminnon run()-metodin toteutusta. Metodin toimintatapa vaihtelee toiminnon mukaan, mutta koska koodi on rakennettu kumottavaksi toimeksi, toiminto voi osallistua ympäristön kumouksen ja uudelleen tekemisen tukeen.

Ympäristössä on kumottavien toimien kehys paketissa org.eclipse.core.commands.operations. Toimen kumous ja uudelleen tekeminen voidaan mahdollistaa toteuttamalla run()-metodin sisässä koodi IUndoableOperation-rajapinnan luontia varten. Toiminnon muunto toimien käyttöön on yksinkertaista lukuun ottamatta kumouksen ja uudelleen tekemisen toteutusta.

Kumottavan toimen kirjoitus

Aluksi tarkastellaan erittäin yksinkertaista esimerkkiä. Readme-esimerkin lisäosassa oli yksinkertainen ViewActionDelegate. Kun toiminto kutsutaan, se vain avaa valintaikkunan, joka kertoo sen ajosta.

public void run(org.eclipse.jface.action.IAction action) {
	MessageDialog.openInformation(view.getSite().getShell(),
		MessageUtil.getString("Readme_Editor"),  
		MessageUtil.getString("View_Action_executed")); 
}
Toimien aikana run-metodi luo toiminnan, joka tekee aiemmin run-metodissa tehdyn työn ja pyytää, että toimien historiatiedot ajavat toimen, että se voidaan muistaa kumousta tai uudelleen tekemistä varten.
public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
Toimi suojaa run-metodilta vanhan toimintatavan ja toimen kumouksen ja uudelleen tekemisen.
class ReadmeOperation extends AbstractOperation {
	Shell shell;
	public ReadmeOperation(Shell shell) {
		super("Readme Operation");
		this.shell = shell;
	}
	public IStatus execute(IProgressMonitor monitor, IAdaptable info) {
		MessageDialog.openInformation(shell,
			MessageUtil.getString("Readme_Editor"),  
		MessageUtil.getString("View_Action_executed"));   
         return Status.OK_STATUS;
	}
	public IStatus undo(IProgressMonitor monitor, IAdaptable info) {
		MessageDialog.openInformation(shell,
			MessageUtil.getString("Readme_Editor"),  
			"Undoing view action");   
         return Status.OK_STATUS;
	}
	public IStatus redo(IProgressMonitor monitor, IAdaptable info) {
		MessageDialog.openInformation(shell,
			MessageUtil.getString("Readme_Editor"),  
			"Redoing view action");   
         return Status.OK_STATUS;
	}
}

Yksinkertaisten toimien osalta kaikki käytännön työ voidaan mahdollisesti siirtää toimiluokkaan. Tässä tapauksessa kannattaa pienentää aiemmat toimintoluokat yhdeksi parametrisoiduksi toimintoluokaksi. Toiminto vain ajaa toimitetun toimen, kun on ajon aika. Tämä on pitkälti sovelluksen luontitapaan liittyvä päätös.

Kun toiminto aloittaa ohjatun toiminnon, toimi luodaan tavallisesti osana ohjatun toiminnon performFinish()-metodia tai ohjatun toiminnon sivun finish()-metodia. finish-metodin muunto käyttämään toimia tapahtuu samalla tavalla kuin run-metodin muunto. Metodi luo ja ajaa toimen, joka tekee aiemmin koodissa tehdyn työn.

Toimien historiatiedot

Edellä on käytetty toimien historiatietoja selittämättä niitä juurikaan. Seuraavassa tarkastellaan uudelleen koodia, joka luo esimerkkitoimen.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
Mitä toimien historiatiedot ovat? IOperationHistory määrittää rajapinnan objektille, joka pitää kirjaa kaikista kumottavista toimista. Kun toimien historiatiedot tekevät toimen, ne ensin tekevät toimen ja sitten lisäävät ne kumouksen historiatietoihin. Työasemat, jotka haluavat tehdä kumouksia tai tehdä toimia uudelleen, tekevät sen IOperationHistory-käytännöllä.

Sovelluksen käyttämät toimien historiatiedot voi noutaa usealla tavalla. Yksinkertaisin tapa on käyttää OperationHistoryFactory-luokkaa.

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

Myös työympäristöä voi käyttää toimien historiatietojen noutoon. Työympäristö määrittää toimien oletushistoriatiedot ja toimittaa myös käytännön niiden käyttöä varten. Seuraava katkelma kuvaa, miten poimitaan toimien historiatiedot työympäristöstä.

IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
Kun toimien historiatiedot on poimittu, niiden avulla voidaan kysellä kumousta tai uudelleen tekemistä, selvittää, mikä toimi on seuraava kumouksen tai uudelleen tekemisen osalta, sekä kumota tai tehdä uudelleen tiettyjä toimia. Työasemat voivat vastaanottaa ilmoituksia historiatietojen muutoksista lisäämällä IOperationHistoryListener-rajapinnan. Muiden käytäntöjen avulla työasemat voivat asettaa rajoituksia historiatiedoille tai ilmoittaa kuuntelutoiminnoille tietyn toimen muutoksista. Ennen käytännön tarkempaa kuvausta on ymmärrettävä kumouskonteksti.

Kumouskontekstit

Toimen luonnin yhteydessä sille asetetaan kumouskonteksti. Se kuvaa käyttäjän kontekstia, jossa alkuperäinen toimi tehtiin. Kumouskonteksti vaihtelee tavallisesti kumottavan toimen synnyttäneen näkymän tai muokkausohjelman mukaan. Esimerkiksi muokkausohjelmassa tehdyt muutokset ovat usein kyseiselle muokkausohjelmalle paikallisia. Tällöin muokkausohjelman tulee luoda oma kumouskontekstinsa ja liittää konteksti toimiin, jotka se lisää historiatietoihin. Tällä tavalla kaikkia muokkausohjelmassa tehtyjä toimia pidetään paikallisina ja puoliyksityisinä. Muokkausohjelmat ja näkymät, jotka toimivat yhteiskäyttömallissa, käyttävät usein kumouskontekstia, joka liittyy niiden käsittelemään malliin. Yleisempää kumouskontekstia käytettäessä yhden näkymän tai muokkausohjelman tekemät toimet voivat olla toisen samaa mallia käyttävän näkymän tai muokkausohjelman kumottavissa.

Kumouskontekstit ovat toiminnaltaan melko yksinkertaisia; IUndoContext-käytäntö on melko minimaalinen. Kontekstin päätehtävänä on "merkitä" tietty toimi kuuluvaksi kyseiseen kumouskontekstiin, että se voidaan erottaa eri kumouskonteksteissa luoduista toimista. Näin toimien historiatiedot voivat pitää kirjaa kaikkien tehtyjen kumottavien toimien yleisistä historiatiedoista, kun näkymät ja muokkausohjelmat voivat puolestaan suodattaa tietyn näkökulman historiatiedot kumouskontekstin käyttöä varten.

Kumottavat toimet luova lisäosa voi luoda kumouskontekstit, tai ne voidaan käsitellä sovellusohjelmaliittymässä. Esimerkiksi työympäristö mahdollistaa sellaisen kumouskontekstin käytön, jota voidaan käyttää työympäristön laajuisiin toimiin. Riippumatta kumouskontekstien poimintatavasta ne tulisi asettaa toimen luonnin yhteydessä. Seuraava katkelma kuvaa, miten readme-lisäosan ViewActionDelegate voi liittää työympäristön laajuisen kontekstin toimiinsa.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
	IUndoContext undoContext = workbench.getOperationSupport().getUndoContext();
	operation.addContext(undoContext);
	operationHistory.execute(operation, null, null);
}

Miksi kumouskonteksteja pitäisi käyttää yleensäkään? Miksei eri näkymille ja muokkausohjelmille voisi käyttää erillisiä toimien historiatietoja? Kun käytetään erillisiä toimintojen historiatietoja, jokaisen näkymän ja muokkausohjelman täytyy pitää yllä omia kumouksen historiatietojaan, eikä kumouksella ole tällöin yleistä merkitystä sovelluksessa. Tämä voi sopia joillekin sovelluksille, ja näissä tapauksissa jokaisen näkymän tai muokkausohjelman tulee luoda oma erillinen kumouskontekstinsa. Toisten sovellusten voidaan haluta toteuttavan yleisen kumouksen, joka koskee kaikkia toimia riippumatta niiden lähteenä olevasta näkymästä tai muokkausohjelmasta. Tällöin työympäristön kontekstin tulisi olla kaikkien historiatietoihin toimia lisäävien lisäosien käytössä.

Monimutkaisemmissa sovelluksissa kumous ei ole tarkkaan ottaen paikallinen eikä tarkkaan ottaen yleinenkään. Sen sijaan kumouskontekstit ovat jonkin verran limittäisiä. Tämän voi toteuttaa liittämällä useita konteksteja toimeen. Esimerkiksi IDE-työympäristön näkymä voi käsitellä koko työympäristöä ja pitää työympäristöä kumouskontekstinaan. Työympäristön tietyssä resurssissa avoinna oleva muokkausohjelma voi pitää toimiaan lähinnä paikallisina. Muokkausohjelman sisällä tehdyt toimet voivat kuitenkin tosiasiassa vaikuttaa sekä kyseiseen resurssiin että myös laajemmin työympäristöön. (Hyvä esimerkki tällaisesta tapauksesta on JDT-koodinparannuksen tuki, joka mahdollistaa rakenteelliset muutokset Java-elementtiin lähdetiedoston muokkauksen yhteydessä). Näissä tapauksissa on hyödyllistä pystyä lisäämään konteksteja sekä toimeen, jolloin kumous voidaan tehdä itse muokkausohjelmasta, että työympäristöä käsitteleviin näkymiin.

Nyt kun kumouskontekstin merkitys on selvillä, voidaan tarkastella jälleenIOperationHistory-rajapinnan käytäntöä. Seuraavan katkelman avulla tehdään kumous tietyssä kontekstissa:

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
   try {
	IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// käsittele poikkeus 
}
Historiatiedot hakevat viimeksi tehdyn toimen, jolla on määritetty konteksti, ja pyytävät sitä kumoamaan itsensä. Toista kontekstia voi käyttää kontekstin koko kumouksen tai uudelleen tekemisen historiatietojen hakuun tai etsimään toimi, joka kumotaan tai tehdään uudelleen tietyssä kontekstissa. Seuraava katkelma hakee tietyssä kontekstissa kumottavan toimen nimiön.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(myContext).getLabel();

Yleisen kumouskontekstin IOperationHistory.GLOBAL_UNDO_CONTEXT avulla voidaan viitata yleisiin kumouksen historiatietoihin. Toisin sanoen kaikkiin historiatietojen toimiin riippumatta niiden kontekstista. Seuraava katkelma hakee yleiset kumouksen historiatiedot.

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
IUndoableOperation [] undoHistory = operationHistory.getUndoHistory(IOperationHistory.GLOBAL_UNDO_CONTEXT);

Aina kun toimi tehdään, kumotaan tai tehdään uudelleen toimen historiatietojen käytännön avulla, työasemat voivat toimittaa tilannetietojen valvontaohjelman ja muut käyttöliittymän tiedot, joita voidaan tarvita toimen tekemiseen. Nämä tiedot välitetään itse toimelle. Alkuperäisessä esimerkissä readme-toiminto muodosti toimen komentoliittymän parametrilla, jonka avulla voidaan avata valintaikkuna. Sen sijaan, että komentoliittymä tallennettaisiin toimeen, parempi käytäntö on välittää parametrit execute-, undo- ja redo-metodeille, jotka toimittavat toimen ajoon tarvittavat käyttöliittymän tiedot. Nämä parametrit välitetään edelleen itse toimeen.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation();
	...
	operationHistory.execute(operation, null, infoAdapter);
}
infoAdapter on IAdaptable-rajapinta, joka voi minimaalisesti toimittaa Shell-komentoliittymän, jota voidaan käyttää valintaikkunoiden avauksen yhteydessä. Esimerkin toiminto käyttäisi tätä parametria seuraavasti:
	public IStatus execute(IProgressMonitor monitor, IAdaptable info) {
	if (info != null) {
		Shell shell = (Shell)info.getAdapter(Shell.class);
		if (shell != null) {
		MessageDialog.openInformation(shell,
			MessageUtil.getString("Readme_Editor"),  
		MessageUtil.getString("View_Action_executed"));   
         return Status.OK_STATUS;
		}
	}
	// tee jotain muuta...
}

Kumous- ja uudelleen tekemistoiminnon käsittelytoiminnot

Ympäristössä on tavalliset kumouksen ja uudelleen tekemisen uudelleen kohdennettavan toiminnon käsittelytoiminnot, jotka näkymät ja muokkausohjelmat voivat määrittää tuottamaan oman kontekstinsa kumoamisen ja uudelleen tekemisen tuen. Kun toiminnon käsittelytoiminto luodaan, sille osoitetaan konteksti niin, että toimien historiatiedot suodatetaan kyseiselle näkymälle sopivalla tavalla. Toimintojen käsittelytoiminnot päivittävät kumoamisen ja uudelleen tekemisen nimiöt näyttämään kyseinen nykyinen toimi, tuottavat sopivan tilannetietojen valvontaohjelman ja käyttöliittymän tiedot toimien historiatietoihin ja valinnaisesti karsivat historiatietoja, kun nykyinen toimi on virheellinen. Käytön helpottamista varten on toimitettu toimintoryhmä, joka luo toimintojen käsittelytoiminnot ja asettaa ne yleisille kumoamisen ja uudelleen tekemisen toiminnoille.

new UndoRedoActionGroup(this.getSite(), undoContext, true);
Viimeinen parametri on looginen arvo, joka osoittaa, tuleeko määritetyn kontekstin kumouksen ja uudelleen tekemisen historiatiedot hävittää, kun kumoukselle ja uudelleen tekemiselle käytettävissä oleva toimi ei ole kelvollinen. Tämä parametrin asetus liittyy annettuun kumouskontekstiin ja kelpuutusstrategiaan, jota toimet käyttävät kyseisessä kontekstissa.

Sovelluksen kumousmallit

Edempänä tarkasteltiin, miten kumouskonteksteja voi käyttää toteuttamaan erilaisia sovelluksen kumousmalleja. Koska konteksteja voi osoittaa toimille, sovellukset voivat toteuttaa kumousstrategioita, jotka ovat tiukasti tietylle näkymälle tai muokkausohjelmalle paikallisia, tiukasti yleisiä kaikissa lisäosissa tai jotain tältä väliltä. Toinen kumoamista ja uudelleen tekemistä koskeva rakenteeseen liittyvä päätös on se, voidaanko jokin toimi kumota tai tehdä uudelleen koska tahansa vai onko malli tiukasti lineaarinen, jolloin vain viimeksi tehty toimi voidaan kumota tai tehdä uudelleen.

IOperationHistory määrittää käytännön, joka sallii joustavat kumousmallit, jolloin yksittäiset toteutukset voivat määrittää, mikä on sallittua. Edellä kuvattu kumouksen ja uudelleen tekemisen käytäntö olettaa, että tietyssä kumouskontekstissa on vain yksi käytettävissä oleva toimi kumousta tai uudelleen tekemistä varten. Käytettävissä on toinen käytäntö, jonka avulla työasemat voivat toteuttaa tietyn toimen riippumatta sen paikasta historiatiedoissa. Toimien historiatiedot ovat määritettävissä niin, että sovellukselle sopiva malli voidaan toteuttaa. Tämä tehdään rajapinnalle, jonka avulla hyväksytään ennalta kumouksen ja uudelleentekemisen pyynnöt ennen toimen kumoamista tai tekemistä uudelleen.

Toimien hyväksyntätoiminnot

IOperationApprover määrittää käytännön tietyn toimen kumouksen tai uudelleen tekemisen hyväksymistä varten. Toimen hyväksyntätoiminto asennetaan toimien historiatietoihin. Yksittäisten toimien hyväksyntätoiminnot voivat tarkistaa kaikkien toimien kelpoisuuden, tarkistaa vain tiettyjen kontekstien toimet tai näyttää kehotteen käyttäjälle, kun toimesta löytyy odottamattomia ehtoja. Seuraava katkelma kuvaa, miten sovellus voi määrittää toimien historiatiedot asettamaan lineaarisen kumousmallin kaikille toimille.
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// aseta historiatiedoille hyväksyntätoiminto, joka estää kumoukset, jotka eivät ole viimeksi tehty toimi
history.addOperationApprover(new LinearUndoEnforcer());

Tässä tapauksessa kehyksen toimittama toimen hyväksyntätoiminto LinearUndoEnforcer asennetaan historiatietoihin estämään sellaisten toimien kumoaminen tai uudelleen tekeminen, jotka eivät ole kaikkien sen kumouskontekstien uusin tehty tai kumottu toimi.

Toinen toimen hyväksyntätoiminto LinearUndoViolationUserApprover havaitsee saman tilan ja kysyy käyttäjältä, tuleeko toimen jatkaminen sallia. Tämä toimen hyväksyntätoiminto voidaan asentaa tiettyyn työympäristön osaan.

IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// aseta tälle osalle hyväksyntätoiminto, joka kysyy käyttäjältä ohjeita, kun toimi ei ole viimeksi tehty.
IOperationApprover approver = new LinearUndoViolationUserApprover(myUndoContext, myWorkbenchPart);
history.addOperationApprover(approver);

Lisäosien kehittäjät voivat kehittää ja asentaa omia toimintojen hyväksyntätoimintojaan sovelluskohtaisten kumousmallien ja hyväksyntästrategioiden toteuttamista varten. Voi olla hyvä edellyttää omassa lisäosassasi, että toiminnon kumoamisen ja uudelleentekemisen lisäksi toiminnon alkuperäinen suoritus on hyväksytettävä. Siinä tapauksessa toiminnon hyväksyntätoiminnon on toteutettava IOperationApprover2-rajapinta, joka hyväksyy toiminnon suorituksen. Kun toiminnon suoritusta pyydetään, käyttöympäristön historiatietotoiminto hakee hyväksynnän miltä tahansa toimintojen hyväksyntätoiminnolta, joka toteuttaa tämän rajapinnan.

Kumous ja IDE-työympäristö

Edellä on tarkasteltu koodikatkelmia, jotka käyttävät työympäristön käytäntöä toimien historiatietojen ja työympäristön kumouskontekstin käyttöön. Tämä tapahtuu käyttämällä IWorkbenchOperationSupport-rajapintaa, joka on saatavissa työympäristössä. Työympäristön laajuinen kumouskonteksti on melko yleinen ajatus. On työympäristön sovelluksen tehtävä määrittää, mitä nimenomaista laajuutta työympäristön kumouskonteksti tarkoittaa ja mitkä näkymät tai muokkausohjelmat käyttävät työympäristön kontekstia toimittaessaan kumouksen tuen.

Eclipse IDE -työympäristön tapauksessa työympäristön kumouskonteksti tulee asettaa kaikille toimille, jotka vaikuttavat IDE-työtilaan laajalti. Tätä kontekstia käyttävät työympäristöä käsittelevät näkymät, esimerkiksi resurssien siirtymisnäkymä. IDE-työympäristö asentaa työympäristöön IUndoContext-sovittimen, joka palauttaa työympäristön kumouskontekstin. Tällaisen mallipohjaisen rekisteröinnin avulla työympäristöä käsittelevät lisäosat voivat hakea sopivan kumouskontekstin, vaikka ne olisivat näyttöpäätteettömiä eivätkä viittaisi mihinkään työympäristöön luokkiin.

// hae toimien historiatiedot
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// poimi sopiva kumouskonteksti mallilleni
IUndoContext workspaceContext = (IUndoContext)ResourcesPlugin.getWorkspace().getAdapter(IUndoContext.class);
if (workspaceContext != null) {
	// luo toimi ja määritä sille konteksti
}

Muiden lisäosien suositellaan käyttävän tätä samaa tekniikkaa mallipohjaisten kumouskontekstien rekisteröintiin.