Source file: /~heha/enas/Convac-Ätzer/Firmware-190517.zip/dlgRezept.cpp

#include "Convac.h"
#include <stdlib.h>	// exit

/*===============*
 * Rezept-Editor *
 *===============*/
 
/* Im Gegensatz zu dlgAnalog und dlgDigital ist dlgRezept sehr umfangreich.
 Die Rezeptliste wird als Baumdiagramm mit schließbaren Knoten angezeigt.
 Die linke Spalte ist reserviert für die Programmlauf-Anzeige (Pfeil),
 der das gerade aktive Rezept (in Ruhe) bzw. den gerade aktiven Schritt
 (in Aktion) anzeigt. Eventuell auch mal Haltepunkte.
 Der übrige Bereich dient als Listenanzeige (mit Rollbalken) für den Baum.
 Alle Elemente können in-place editiert, hinzugefügt oder gelöscht werden,
 auch während des Programmablaufs.
 Während es für Rezepte nur die Eingabe der ID
 und für Schritte nur die Eingabe der Zeit gibt,
 gibt es für Befehle /vier/ Eingabeelemente:
  - Bedingung oder (bedingungslose) Ausgabe-Aktion
  - Aktion bei Nichterfüllung der Bedingung
  - Betreffender Analog- oder Digitalkanal
  - Wert (zu prüfen oder auszugeben)
 Im Programmlauf werden Digitalwerte sofort
 und Analogwerte in einer linearen Rampe über die Dauer des Schritts ausgegeben.
 Geprüft werden Analog- und Digitalwerte permanent während der Schritt-Zeit.
 Rezepte können keine Schleifen enthalten, es sind allesamt Sequenzen.
*/

// Endadresse im „dynamischen“ Speicher
static byte*gsend() {return ((byte*)&eedata.gs)+gslen;}

// Die beiden „fundamentalen“ Werte des dynamischen Speichers berechnen:
// rl = Anfang der Rezeptliste
// gslen = Länge der dynamischen Daten
word GS::calcsizes() {
 rl=eedata.gs.fin();
 return gslen=rl->size()+(byte*)rl-(byte*)&eedata.gs;
}

// „Dynamischen“ Speicher bewegen: Platz machen oder Platz sparen.
// Neuer Speicher wird mit Null gelöscht.
// Liefert false wenn kein Platz
bool GS::move(byte*dst,byte*src) {
#ifdef DEBUG
 if (dst<=(byte*)&eedata.gs) exit(-21);
 if (dst>(byte*)&eedata+sizeof eedata) exit(-22);
 if (src<=(byte*)&eedata.gs) exit(-21);
 if (src>(byte*)&eedata+sizeof eedata) exit(-22);
#endif
 int diff=dst-src;
 if (!diff) return true;	// Nichts tun: Quelle == Ziel
 if (diff>int(sizeof eedata-sizeof(CONFIG)-gslen)) return false;
 word l=gsend()-src;		// Länge der zu schaufelnden Daten in Bytes
 memmove(dst,src,l);		// (l kann Null sein)
 if (diff>=0) memset(src,0,diff);
 return true;
}

// Speicherbereich in-place links rotieren (nicht zwingend im Dynamischen Speicher)
// eadr = letzte Speicherzelle, NICHT darüberhinaus
// len = Anzahl der byteweisen Linksschiebeoperationrn
// Kode-Kürze geht hier vor Performance!!
static void memrol(byte*aadr,byte*eadr,word cnt) {
#if 1
 asm(
"0:	movw	r30,%A0		\n"
"	ld	r1,Z		\n"
"1:	ldd	r0,Z+1		\n"
"	st	Z+,r0		\n"
"	cp	r30,%A1		\n"
"	cpc	r31,%B1		\n"
"	brne	1b		\n"
"	st	Z,r1		\n"
"	subi	%A2,1		\n"
"	sbci	%B2,0		\n"
"	brne	0b		\n"
"	clr	r1		\n"
 ::"r"(aadr),"r"(eadr),"r"(cnt):"r30","r31");
#else
 do{
  byte save=*aadr;
  memcpy(aadr,aadr+1,eadr-aadr);
  *eadr=save;
 }while(--cnt);
#endif
}

/* Der Rezept-Editor verwendet ebenfalls die ListView wie der Analog-Editor,
   jedoch als Baumdiagramm mit auf- und zuklappbaren Knoten.
 */

static void plusminus(byte x, N n) {
 d.X=x; d.print(n.f?0x1B:0x1A);	// Eingerahmtes Minus oder Plus
}

// generiert Blinken und führt das Fokus-Sichtbarkeitsbit mit
static void xorFocus() {
 lvXorFocus(C.focusr);
}
// Fokusrechteck weg (vor dem Neuzeichnen von _Teilen_ des invaliden Bereichs)
inline void hideFocus() {if (focusVis) xorFocus();}

// Fokusrechteck anzeigen (nach dem Neuzeichnen des invaliden Bereichs)
inline void showFocus() {if (!focusVis) xorFocus();}

// Aufruf durch Ändern des aktuellen Rezepts
// flags.0: Rezept oder Schritt markieren
// flags.7: Von paint(), Clipping-Bereich nicht aufreißen
static void xorMarker(byte flags=0) {
 ITER iter;		// Lokales <iter> (6 Bytes)
 iter.ire=flags&3?run.rezept:C.runrez;
 iter.ist=run.aktion;
 iter.type=flags&3;
 byte line=iter.line(); // Zeile ermitteln
 if (line==0xFF) return;	// kann nur bei flags&3!=0 passieren
 if (!(flags&0x80)) lv.setFullClip();
 flags&=3;
 d.flags=d.COLOR|d.R2_XORPEN;
 int y=lvScreenY(line);
 if (y<-10) return;	// außerhalb oben (kann aber partiell gemalt werden!)
 if (y>64) return;	// außerhalb unten
 d.gotoXY(1+(flags<<2),y);
 d.print(0x1E);
}

// Arbeitet mit lokaler <iter>-Struktur beim Durchlaufen
static void treepaint() {
 markNo();		// Bild ungültig, idle() darf nicht hineinmalen
 byte line=0;
 N nSt;
 d.flags=d.COLOR|d.R2_COPYPEN;
 ITER iter;		// Lokales <iter> (6 Bytes)
 if (showSafe) {iter.initgs(); iter.ire=0xFF; nSt.b=1; goto innen;}
aussen:
 iter.init();	// dreistufiger Baum
 for (iter.ire=0; iter.ire<rl->nRezept;) {
  nSt=iter.re->n;
  if (lv.itemInvalid(line)) {
   d.Y=lvScreenY(line);
   if (nSt.c>!showSafe) plusminus(6,nSt);
   d.X=16;	// 1 Pixel Luft
   iter.re->print(iter.ire);
  }
  line++; idle();
  iter.st=iter.re->st0();		// jetzt Zeiger auf SCHRIT
innen:
  for (iter.ist=0; iter.ist<nSt.c; iter.ist++) {
   N nBe;
   nBe.b=iter.st->n.b|nSt.b&0x80;
// für den Compiler umständlicher: nBe=iter.a->n; nBe.f|=nSt.f;
   if (showSafe||iter.ist) {		// Schritt-Zeile darstellen
    if (!nSt.f) {			// (wenn Rezept aufgeklappt)
     if (lv.itemInvalid(line)) {
      d.Y=lvScreenY(line);
      if (nBe.c) plusminus(14,nBe);
      d.X=24;	// 1 Pixel Luft
      iter.st->print(iter.ist);
     }
     line++; idle();
    }
   }else{		// Schritt-Zeile nicht darstellen
    nBe.b|=0x80;	// Befehlsausgabe (weiter unten) unterdrücken
   }
   iter.be=iter.st->be0();		// jetzt Zeiger auf BEFEHL
   for (iter.ibe=0; iter.ibe<nBe.c; iter.ibe++) {
    if (!(nBe.f)) {
     if (lv.itemInvalid(line)) {
      d.gotoXY(32,lvScreenY(line));
      iter.be->print(iter.ibe);
     }
     line++; idle();
    }
    iter.be=iter.be->fin();
   }/*nBe*/
  }/*nSt*/
  if (!++iter.ire) goto aussen;	// weiter mit Rezeptliste (nRezept überspringen)
 }/*nRezept*/
 markYes();	// Bild ist gültig, idle() darf hineinmalen
 xorMarker(0x80);
 if (run.aktion) xorMarker(0x81);
 xorFocus();	// immer vollständig sichtbar
}

// Hinterlässt iter mit dem richtigen Zeiger für C.focusr
// und den Indexeinträgen ire, ist und ibe (soweit zutreffend).
// Setzt den Typ des Zeigers, 0 = REZEPT*, 1=SCHRIT*, 2=BEFEHL*
// In Globaler Sicherheit ist .ire==0xFF und .ist==n; .type = 1 oder 2
void ITER::whereami() {
 byte cnt=C.focusr+1;
 N nSchrit;
 if (showSafe) {initgs(); ire=0xFF; nSchrit.b=1; goto innen;}
aussen:
 init();
 for (ire=0;ire<rl->nRezept;) {
  type=0;
  if (!--cnt) return;			// Rezepte sind immer sichtbar
  nSchrit=re->n;
  st=re->st0();
innen:
  for (ist=0;ist<nSchrit.c;ist++) {
   type=1;
   if (!nSchrit.f && (showSafe||ist)  && !--cnt) return;	// nur zählen wenn aufgeklappt
   N nBefehl;
   nBefehl.b=st->n.b|nSchrit.b&0x80;	// Zuklapp-Bits kombinieren
   be=st->be0();
   type=2;
   for (ibe=0;ibe<nBefehl.c;ibe++) {
    if (!(nBefehl.f) && !--cnt) return;	// nur zählen wenn aufgeklappt
    be=be->fin();
   }// am Ende dieser Schleife steht der Zeiger auf SCHRIT*
  }// am Ende dieser Schleife steht der Zeiger auf REZEPT*
  if (!++ire) goto aussen;	// weiter mit Rezeptliste (nRezept überspringen)
 }// am Ende dieser Schleife steht der Zeiger am Ende der Rezeptliste
 type=0xFF;	// nicht gefunden! (Bspw. hinter allen Einträgen)
}

// liefert Zeilennummer, 0xFF bei Fehler
// Bei <lastindex> == 0 für .ire (auf Rezept)
// Bei <lastindex> == 1 für .ire und .ist (auf Schritt)
// Da der gesuchte Schritt in einem zugeklappten Rezept liegen kann, gibt's dann 0xFF zurück.
byte ITER::line() {
 if (type==0xFF) return -1;	// ungültig
 byte i;
 byte ret=0;
 N nSchrit;
 initgs();			// früherer Start
 if (ire==0xFF) {
  if (!showSafe) return -1;	// nicht zu sehen
  nSchrit.b=1;			// (übergeordnetes gedachtes Rezept stets aufgeklappt)
 }else{
  if (showSafe) ret=st->getLines();
  init();
  for (i=0;i<ire;i++) {	// Rezepte immer zählen, keine Überlaufprüfung!
   ret+=re->getLines();	// okkupierte Treeview-Zeilen zählen
   re=re->fin();		// zum nächsten Rezept
  }
  nSchrit=re->n;		// Anzahl Aktionen
 }
 if (type>=1) {			// im richtigen Rezept auf Aktion kommen
  if (nSchrit.f) return -1;	// Rezept nicht aufgeklappt!
  ret++;			// Rezept-Zeile zählen
  st=re->st0();		// Zeiger auf erste Aktion
  for (i=0;i<ist;i++) {
   ret+=st->getLines();
   st=st->fin();
  }
  if (type>=2) {
   if (st->n.f) return -1;	// Nicht aufgeklappt!
   ret++;			// Aktion-Zeile zählen
   be=st->be0();
   for (i=0;i<ibe;i++) {
    ret+=be->getLines();	// liefert zurzeit stets 1
    be=be->fin();
   }
  }
 }
 return ret;
}

// benutzt nur .ire und .ist um den Zähler zu adressieren,
// der Zeiger wird nicht zerstört
// Liefert eine _Referenz_
N&ITER::getParentCounter(byte level) {
 if (ist==0xFF) return eedata.gs.st.n;	// Anzahl Sicherheitsaktionen
 if (!level) return *(N*)(&rl->nRezept);// Anzahl Rezepte (stets "aufgeklappt")
 REZEPT&r=(*rl)[ire];	// (Achtung! operator[])
 if (level==1) return r.n;	// Anzahl Schritte des Rezepts (wenn auf "Schritt")
 return r[ist].n;	// Anzahl Einzelaktionen des Schritts (wenn ist==2) (Achtung! operator[])
}

// Liefert den aktuellen Unterknotenzähler als Referenz
N&ITER::getCounter() {
 switch (type) {
  case 0: return vre->n;
  case 1: return vst->n;
  default: exit(-5);
 }
}

const void*rsbNext(const void*p,byte type) {
 switch (type) {
  case 2: return ((const BEFEHL*)p)->fin();	//nächster Befehl oder Ende der Kette
  case 1: return ((const SCHRIT*)p)->fin();	//nächster Schritt oder Ende der Kette
  case 0: return ((const REZEPT*)p)->fin();	//nächstes Rezept oder Ende der Kette
  default: exit(-6);
 }
}

static ITER riter;	// Iterator des Rezet-Editors

// Kanalname als Überschrift ausgeben
static void showName() {
 const char*q=NULL;
 switch (riter.type) {
  case 0: q=F("(Rezept)"); break;
  case 1: q=F("(Schritt)"); break;
  case 2: {
   byte i=riter.be->index;
   if (riter.be->operation&3) {
    q=FP(Na);	// Analogkanäle (max. 256 ohne Gruppen)
   }else{
    q=(const char*)pgm_read_word(Nlist+(i>>5));
    i&=0x1F;	// Digitalkanäle (32 pro Gruppe = E/A-Karte)
   }
   q=strend1(q,i);
  }break;
 }
 centerTitle(q);
}

// Aufruf durch Hintergrundprozess, also idle()
void xorAktionMarker() {
 if (!mark) return;	// nicht erlaubt
 xorMarker(true);
}

static void paintHeader() {
 if (!zoom) {
  byte nRezept=rl->nRezept;
  d.print(floatstr(nRezept),0,0);
  d.print(F(" Rezept"));
  if (nRezept!=1) d.print('e');
  onZoomClear();
  showName();
 }
}

#define rlines GPIOR1	// Rezept-Zeilenzahl

static byte getLineMax() {	// Maximale Zeilennummer
 return rlines?rlines-1:0;
}

static void prepaint() {
 if (allNew) {
  rlines=eedata.gs.getLines();
  byte max=getLineMax();
  if (C.focusr>max) C.focusr=max;
  riter.whereami();
  paintHeader();
 }
}

/*
Gemeinsame Malroutine für die verschiedenen Editier-Subdialoge
Verwendet die Flags #, reLine, redraw und löscht am Ende alle drei.
newLen	reLine	redraw	Inval	Aktion
0	0	0	d.clip	nur showFocus
0	0	1	d.clip	d.clip restaurieren
0	1	0	-	Aktuelle Zeile (C.focusr) malen — muss komplett sichtbar sein!
0	1	1	d.clip	[+]/[-] übermalen UND d.clip restaurieren
1	x	x	-	Länge <rlines> setzen und komplett neu malen
*/
static void repaint() {
 //auto t=d.T,b=d.B;		// Retten für reLine && redraw
 byte y=lvScreenY(C.focusr);	// passt hier stets ins Byte
 if (allNew) {
// Um Kodespeicher zu sparen, ist die Anzahl der darstellbaren
// Programmzeilen auf 254 begrenzt.
// Der EEPROM kann jedoch etwas mehr als diese Zeilenzahl an Kode enthalten.
// Der Anwender muss deshalb einige Knoten geschlossen halten,
// sonst gibt es Darstellungsfehler.
  lvInit(C.focusr,getLineMax());
  clrAllNew();
  goto all;
 }
 if (reLine) {			// [+]/[-] geändert?
  if (redraw) {
   hideFocus();			// erforderlich weil [+]/[-] Fokusrechteck löchert
   d.flags=d.R2_COPYPEN;
   d.Y=y;
   d.saveClrClip();
   plusminus(riter.type?14:6,riter.getCounter()); // [+]/[-] aktualisieren
   d.restoreClip();
   goto all;
  }
  d.setClip(lv.L,y,lv.R-lv.SBW,y+lv.dih-1);
  goto all;		// Aktuelle Zeile komplett neu malen
 }
 if (redraw) {	// Cursor ist bereits weg
all:
  ticStart();
  d.flags=d.R2_ZERO;
  d.fillClip();		// invaliden Bereich löschen
  treepaint();		// invaliden Bereich malen
 }
 clrRedraw();
 clrReLine();
 showFocus();
}

// Editor-Top+Bottom setzen
static void setTB() {
 ed.B=(ed.T=lvScreenY(C.focusr))+lv.dih-1;	// unten und oben
 ed.cursor=0xFF;
 ed.maxlen=5;
 ed.setText(sbuf);	// markiert alles
}

// Modaler Zeileneditor für ID (eines Rezepts), Zeitdauer (eines Schritts)
// und (analoge oder digitale) Werte (eines Befehls)
// Nie im Zoom-Modus! <action> = gedrückte Taste, die durchgereicht wird
static void dlgValue(byte action=0) {
 ed.L=(ed.R=239-lv.SBW)-39;			// rechtsbündig am Rollbalken
 byte mask;
 switch (riter.type) {
  case 2: riter.be->sprintVal(false);
  mask=riter.be->vlen()?ANALOG::scale[riter.be->index].nk?0x3B:0x2B:0x23;
  break;
  case 1: floatstr(riter.st->ms10,2); mask=0x33; break;	// Dauer: Kein „Minus“
  default: floatstr(riter.re->st[0].ms10); mask=0x23; break;	// ID: Weder „Minus“ noch „Komma“
 }
 setTB();
//  d.clrClip();
//  clearMenu();
//  if (C.anapos==1 || !ANALOG::scale[C.focusa].nk) mask&=~0x10; // kein "Komma"
 paintMenu(F("\x1F\0\x1E\0\0Minus\0Komma\0\x7F"),mask,0x3F);
//  showEditHelp();  
 if (!action) ed.update();
 for(;;action=0) {
  if (!action) action=getkey(50);	// mit 2 Hz Nullen generieren
  switch (action) {
   case '\n': {		// SET: Wert übernehmen und zurück
    switch (riter.type) {
     case 0: {
      int v=strfloat(ed.text);
      if ((unsigned)v>=32767) goto fail;	// Inf, NaN und negativ nicht erlaubt
      riter.vre->st[0].ms10=v;
     }break;
     case 1: {
      int v=strfloat(ed.text,2);
      if ((unsigned)v>=32767) goto fail;
      riter.vst->ms10=v;
     }break;
     case 2: if (!riter.vbe->sscanVal(ed.text)) goto fail; break;
    }
   }return;
   case '\r': return;	// Eingaben verwerfen und zurück
   default: {
    if (action&0x80) action=transcode(action-0xF1);
    if (action>=' ') ed.removeLeadingZero(); // Sinnlose führende Null entfernen
    if (!ed.handleInput(action)) fail:errorbeep(); continue;
   }
  }
 }
}

// Eingabe einer Kanalnummer eines Befehls (nicht der Wert derselben)
// Bedingung: riter.type==2, zoom==false
static void dlgKanal() {
 ed.L=7;
 ed.R=24;
 paintMenu(F("\x1F\0\x1E\0\x1C\0\x1D\0\0\x7F"));
m1:
 paintMenuItem(4*40,F("analog"),riter.be->vlen()?25:24);
m2:
 setFocusVis();		// Verhindere Zeichnen des Rahmens in repaint()
 repaint();
 riter.be->sprintKanal(false);
 setTB();
 ed.update();
 for(;;) {
  byte action=getkey(50);	// mit 2 Hz Nullen generieren
  switch (action) {
   case '\n': {			// SET: Wert übernehmen
    if (!riter.vbe->sscanKanal(ed.text)) goto fail;	// Kanalnummer setzen
    setReLine();
    showName();
   }goto m2;
   case '\r': return;		// Aufrufer muss restaurieren (kein Undo möglich)
   case 0xF3:
   case 0xF4: {
    byte max=(riter.be->vlen()?ANALOG::getMax():DIGITAL::getMax()+1<<3)-1;
    byte j=riter.be->index;
    do{
     j+=action==0xF3?1:-1;
     if (j>max) j=action==0xF3?0:max;
    }while(!(riter.be->vlen()||shrb(DIGITAL::getAvail(j>>3),j)&1));
    riter.vbe->index=j;		// neuen Index setzen
    setReLine();
    showName();			// Name ändert sich!
   }goto m2;			// Numerische Angabe ändert sich!
   case 0xF5: {
    if (!riter.vbe->swapAna()) goto fail;
    setReLine();
    showName();			// Name ändert sich!
   }goto m1;			// Menü ändert sich!
   default: {
    if (action&0x80) action=transcode(action-0xF1);
    if (!ed.handleInput(action)) fail: errorbeep(); continue;
   }
  }
 }
}

// Scrollt den List-Bereich ab C.focusr nach unten (dy<0) oder oben (dy>0).
// Die Scrollbar wird erneuert. Das Redraw-Flag wird faktisch stets gesetzt.
// Clipping-Rechteck (d.clip) = neu auszumalender Bereich.
static void collapse(char dy) {
 d.clrClip();
 lv.collapse(lvPixelY(C.focusr),lv.dih*dy);
}

// Neu malen mit Scrollen bei Einfüge- oder Löschposition
// <rlines> wird neu berechnet und anhand der Differenz zu vorher gescrollt
static void insdel() {
 auto before=rlines;
 rlines=eedata.gs.getLines();
 collapse(before-rlines);
}

// 1 Item einfügen, <type> für den Eltern-Zähler (via riter) erforderlich
static bool insert1(byte*pos,word len,byte type) {
 N&n=riter.getParentCounter(type);	// „auto“ geht hier nicht!!
 if ((n.b&0x3F)==0x3F) return false;	// Maximalzahl an Rezepten, Schritten oder Befehlen (63) erreicht
 if (!eedata.gs.move(pos+len,pos)) return false;	// Platz machen und Pointer neu berechnen
 n.b++;
 ALARM::move(pos+len,pos);	// Alarm-IDs (= Befehls-Adressen) verschieben
 return true;
}

// Modaler Unterdialog, fragt nach dem Typ des neuen Knotens, und fügt diesen ein.
// Um zu sehen, was gleich passiert, wird entsprechend Platz geschaffen.
// Das Fokusrechteck muss vom Aufrufer entfernt worden sein.
// (Die fokussierte Zeile ist vollständig sichtbar.)
// Aufruf verboten für globale oder lokale Sicherheit,
// denn davor darf kein Schritt eingefügt werden.
static void dlgNeu() {
 ITER niter=riter;
 byte a=riter.type;
 if ((char)a<0) a=1;		// Leerzeile? Nur „Rezept“ erlaubt
 else{
  ++C.focusr;
  niter.whereami();		// Typ und Zeiger der Folgezeile ermitteln
  a=7;
  switch (niter.type) {
   case 2: a&=4; break;		// Folgezeile Befehl: Niemals Schritt oder Rezept einfügen
   case 1: a&=6; break;		// Folgezeile Schritt: Niemals Rezept einfügen
  }
  switch (riter.type) {
   case 1: if (riter.st->n.b && niter.type!=2) a&=3; break;
	// Schritt-Befehl: Befehl erlauben
	// Schritt(0)-*: Befehl erlauben
	// Schritt-*: Befehl verbieten
   case 0: a&=1;		// Rezept(0) gibt's nicht, Rezept-Befehl gibt's nicht.
	// Rezept-*: Befehl und Schritt verbieten
	   if (riter.re->n.c==1 && (char)niter.type<=!showSafe) a|=2; break;	
	// Rezept(1)-Schritt: Schritt erlauben NUR wenn showSafe=0
	// Rezept(1)-Rezept, Rezept(1)-nichts: Schritt erlauben
  }
 }
 lv.scrollIntoView(C.focusr);	// Folgezeile sichtbar machen wenn unten
 collapse(-1);			// Platz machen (herunterscrollen)
 d.B=d.T+lv.dih-1;
 d.fillRect();			// schwarz
 d.flags=d.R2_NOTCOPYPEN;
 d.print(F("Neu?"),80,d.T);	// weiß auf schwarz
 d.flags=d.R2_COPYPEN;
 paintMenu(F("Rezept\0Schritt\0Befehl"),a,0x3F);
rept:
 byte action;
 switch (action=getkey()) {
  case 0xF1: if (!(a&1)) goto err; break;
  case 0xF2: if (!(a&2)) goto err; break;
  case 0xF3: if (!(a&4)) goto err; break;	// auf Level = 0, 1 oder 2
  case '\r': {			// Irrtum, keine neue Zeile
   d.flags=d.R2_COPYPEN;	// weiß ausmalen
   collapse(1);		// zurückscrollen
   if (C.focusr) --C.focusr; 
   repaint();			// restaurieren
   hideFocus();
   return;			// Aufrufer restauriert Menüzeile
  }
  default:err:errorbeep(); goto rept;	// Alle anderen Tasten (auch Ziffern) anmeckern
 }
 a=action-0xF1;			// 0 = Rezept, 1 = Schritt, 2 = Befehl
 if (!insert1(niter.vb,4-a,a)) { // itemlen: 2 Bytes für DigitalIO, 3 für Schritt, 4 für Rezept inklusive Sicherheitsschritt: Klappt so mit Subtraktion!
  errorbeep();
  return;
 }
 if (!a) {			// neues Rezept? (Sonst Null belassen)
  niter.vre->n.b=1;		// sofort einen (leeren) Sicherheitsschritt anlegen
  *(byte*)&niter.vre->st[0].ms10=ticker.b;	// ID (irgendetwas) eintragen
 }				// Alle anderen neuen Items bleiben mit 0 initialisiert
 eedata.gs.calcsizes();		// neu ausrechnen (jetzt erst!)
 setAllNew();			// Aufrufer restauriert
}

// Kein Dialog! Rezept, Schritt oder (nicht verwendet:) Befehl (nach hinten) kopieren
static void Duplicate() {
 byte*end=(byte*)riter.next();
 if (!insert1(end,end-riter.vb,riter.type)) {
  errorbeep();
  return;
 }
 memcpy(end,riter.vb,end-riter.vb);	// Kopiervorgang
 eedata.gs.calcsizes();		// neu ausrechnen (jetzt erst!)
 setAllNew();			// Aufrufer restauriert
}

static void Swap(bool down) {
 if (!down) {
  riter.i[riter.type]--;
  C.focusr=riter.line();
 }
 byte*src=riter.vb;
 byte*dst=(byte*)riter.next();
 byte*end=(byte*)rsbNext(dst,riter.type);
 memrol(src,end-1,dst-src);	// Normalerweise müssten jetzt noch die Alarm-IDs verschoben werden: Kein Platz im Flash!
 if (down) {
  riter.i[riter.type]++;
  C.focusr=riter.line();
 }
 setAllNew();
}

// Modaler Unterdialog, fragt ob wirklich gelöscht werden soll, und löscht den aktuellen Knoten.
// Aufruf verboten für globale oder lokale Sicherheit sowie für die Leerzeile.
static void dlgDelete() {
 byte a=riter.type;
 if (a!=2) {		// Zusätzliche Rückfrage wenn ganze Äste beteiligt sind (TODO: Subitems zählen!)
  d.clrClip();
  d.flags=d.R2_NOTCOPYPEN;
  d.print(F(" Löschen? "),80,lvScreenY(C.focusr));	// weiß auf schwarz über das Item
  d.flags=d.R2_COPYPEN;
  if (!yesno()) {
   setReLine();		// Aufrufer restauriert aktuelle Zeile und Menü
   return;		// Irrtum, nicht löschen
  }
 }
 riter.getParentCounter().b--;	// kann nicht zugeklappt sein (niemals darf 0x80 herauskommen)
 byte*src=(byte*)riter.next();
 eedata.gs.move(riter.vb,src);	// memmove-Quelladresse bestimmen und alles dazwischen löschen
 ALARM::move(riter.vb,src);	// Alarm-IDs (= Befehls-Adressen) verschieben
 eedata.gs.calcsizes();		// neu ausrechnen!
 if (C.focusr) --C.focusr;	// Zur vorherigen Zeile gehen
 setAllNew();			// Aufrufer restauriert alles (Nummerierung und Numerale ändern sich)
}

// Modaler Unterdialog zum Verändern eines Befehls
// sowie zum Anlegen neuer Zeilen und Löschen vorhandener Zeilen.
// Löscht den Bildschirm nicht, sondern setzt auf den von dlgRezept auf.
// Niemals im Zoom-Modus!
static void dlgEdit() {
 clrRedraw();
menu:
 prepaint();
 byte mask=0x37;
 if (riter.type!=2) mask=0x30;	// „Klausel+“, „Aktion+“ und „BitNr.“ nur für Befehl
// Sonderfall Sicherheitsliste: Kein Einfügen davor, kein Löschen (nur „Dauer“ möglich)  
// if (riter.type==1 && !riter.ist) mask=0x08;
// change = 0x3F: Rahmen nicht neu zeichnen  
 paintMenu(F("Klausel\025\0Aktion\025\0Kanal\0\0Neu\0\x7F"),mask,0x3F);
 paintMenuItem(3*40,strend1(F("ID\0Dauer\0Wert"),riter.type));
 if (riter.type<2) {
  mask=7;
  auto i=riter.i[riter.type];
  if (i<=riter.type) mask&=~1;// Kein „nach oben tauschen“
  if (i>=riter.getParentCounter().b-1) mask&=~2;	// Kein „nach unten tauschen“
  paintMenu(F("\x1C\x1C\0\x1D\x1D\0× 2"),mask,7);
 }
paint:
 clrFocusVis();			// Fokus permanent anzeigen lassen
 repaint();
 byte action;
 for(;;) switch (action=getkey()) {
  case 0xF1: if (!(mask&1)) goto err; goto gem;
  case 0xF2: if (!(mask&2)) goto err; gem:
  if (riter.type==2) {		// Befehl
   byte op=riter.vbe->operation;
   if (action==0xF1) {
    if (op&7) op=op&0xE0|op+8&0x18;	// Analog: Eine von 4 Operationen auswählen (2 sichtbar)
    else op^=0x10;		// Digital: Eine von 2 Operationen auswählen (keine sichtbar)
   }
   else op+=0x20;			// Eine von 8 Aktionen auswählen
   riter.vbe->operation=op;
   setReLine();
   goto paint;
  }
  Swap(action-0xF1);
  goto menu;
  case 0xF3: if (!(mask&4)) goto err;
  hideFocus();
  if (riter.type==2) {		// Befehl: Kanalwahl
   dlgKanal();
   setReLine();			// Zeile neu malen um Edit-Element zu löschen
   goto menu;
  }
  Duplicate();
  goto menu;
  case '\n':			// Idee für SET: Befehl testen
  case 0xF4: action=0;
  if (riter.type==2 && !riter.be->vlen()) {
   hideFocus();
   riter.vbe->operation^=OP_D1^OP_D0;	// Sonderfall Binär: Toggle
   setReLine();
   goto paint;
  }goto def;
  case 0xF5: {					// Neue Zeile einfügen
   hideFocus();
   dlgNeu();
  }goto menu;
  case 0xF6: {	// Löschen (ganzer Äste!)
   if ((char)riter.type<0) goto err;		// Markierung auf Leerzeile
   if (riter.type==1 && !riter.ist) goto err;	// Sicherheitsschritt oder Globale Sicherheit löschen ist verboten
   dlgDelete();
  }goto menu;
  case '\r': {
   hideFocus();
  }return;
  default: def:{
   hideFocus();
   dlgValue(action);
   setReLine();
  }goto menu;
err: errorbeep(); continue;
 }
}

// Prüft ob [+]/[-] davorgesetzt werden soll:
static bool menuPlusMinus() {
 switch (riter.type) {
  case 0: if (riter.re->n.c>!showSafe) return true;
  case 1: if (riter.st->n.c) return true;
 }
 return false;
}
static void paintMenuPlusMinus() {
 paintMenuItem(2*40,menuPlusMinus()?F("\x1A \x1B"):NULL);
}

// Prüft ob die aktuelle Zeile „Edit“ oder nur „Neu“ zulässt
// (Ein Rezept _vor_ alle anderen Rezepte lässt sich nur
//  bei sichtbaren Sicherheitsschritten einfügen.)
static bool menuEdit() {
 switch (riter.type) {
  case 1: if (!riter.ist) break;	// Sicherheitsschritte sind weder editier- noch löschbar
  case 0:		// Jeder Befehl kann editierbare ID haben und ist löschbar
  case 2: return true;	// Alle Schritte sind editier- und löschbar
 }
 return false;		// Sicherheitsschritte und die Leerzeile lassen nur „Neu“ zu
}
static void paintMenuEdit() {
//   if (riter.type==1 && !riter.ist) goto err;	// Kein Editieren Sicherheitsschritt
 paintMenuItem(3*40,menuEdit()?F("Edit"):F("Neu"));
}

void dlgRezept() {
 clrFocusVis();
vorn:			// Bei Änderung von "zoom" und "showSafe" (selten)
// Kompletter Bildaufbau: Kopf, Zeilenzahl, whereami, Liste ...
 d.clear();
 setAllNew();		// Alles bei repaint() neu malen
// Menü neu zeichnen (nach Sub-Dialog)
menu:			// Menü restaurieren (nach Sub-Dialogen)
 prepaint();
 if (!zoom) {
  d.saveClrClip();
  paintMenu(F("\x1C\0\x1D"),0x03);
  paintMenuPlusMinus();
  paintMenuEdit();
  paintMenuItem(4*40,F("Safe"),showSafe?25:24);	// mit Checkmark
  paintMenuZoom();
  d.restoreClip();
 }
paint:			// Neu malen (je nach Flags newLen, redraw und reLine)
 repaint();
rept:
// mit 2 Hz Nullen generieren, sonst idle() aufrufen
 switch (byte action=getkey(50)) {
  case 0: {
   if (ticCheck(50)) xorFocus();
  }goto rept;
  case '\n': switch (riter.type) {	// SET = auswählen
   case 0: {		// Rezept: Aktuelles Rezept festlegen
    if (riter.ire==C.runrez) {
     if (run.aktion) run.aktion=(run.re->n.c)-1; // Läuft bereits: zu Ende führen!
     else run.start(C.runrez);		// Ist bereits aktiv: starten!
     goto rept;
    }
    hideFocus();
    byte n=riter.ire;
    xorMarker();
    C.runrez=n;
    xorMarker();
   }goto rept;
  }err: errorbeep(); goto rept;
  case '\r': goto exi;
  case 0xF1:	// hoch
  case 0xF2: {	// runter
   hideFocus();
   C.focusr=updown(C.focusr,action,getLineMax());
   riter.whereami();	// <riter> neu berechnen
   if (!zoom) {
    showName();	// neuer Header
    paintMenuPlusMinus();
    paintMenuEdit();
   }
   d.clrClip();
   lv.scrollIntoView(C.focusr);
  }goto paint;
  case 0xF3: {	// plus/minus
   if (!menuPlusMinus()) goto err;	// keine Funktion wo kein [+]/[-] steht
   hideFocus();
   riter.getCounter().b^=0x80;
   ++C.focusr;			// Scrollpunkt dahinter
   insdel();			// dadurch neue Länge
   --C.focusr;
   setReLine();			// [+]/[-] neu malen lassen
  }goto paint;
  case 0xF4: {
   if (zoom) goto err;
   hideFocus();
   if (menuEdit()) dlgEdit();
   else dlgNeu();		// Kann [+]/[-] darüber erzeugen u.v.a.m.!
  }goto menu;
  case 0xF5: {			// Umschalten des showSafe-Bits
   xorShowSafe();
   byte line=riter.line();
   if ((char)line<0) line=0;	// kann bei showSafe==0 passieren
   C.focusr=line;
  }goto vorn;
  case 0xF6: xorZoom(); goto vorn;
  default: {	// Edit
   if (zoom) goto err;
   hideFocus();
   dlgValue(action);		// ID, Dauer oder Wert eingeben
   clrFocusVis();
   setReLine();
  }goto menu;
 }
exi:
 markNo();	// Ende der Darstellung, keine Dreiecke xoren
}
Detected encoding: UTF-80