MaCocoa 057

Capitolo 057 - Visto, si stampi

Un capitolo sofferto, non avete idea. Un sacco di problemi, che non ho capito, che ho forse risolto, ma che comunque non mi piacciono. Tutto parte dall'idea di stampare il contenuto delle finestre in modo acconcio. La cosa si rivela piuttosto semplice per la finestra della copertina, mentre una stampa accettabile del catalogo (la lista dei file) è stato un incubo. Tanto tempo perduto, ma, come succede sempre in questi casi, ho imparato molte cose. La cosa più strana, c'è poco codice, ma tante spiegazioni da dare.

Sorgenti: la documentazione Apple, ma potrei essere incocciato in qualche errore.

Prima stesura: 9 settembre 2004.

Le classi della stampa

Per effettuare le operazioni di stampa, Cocoa mette a disposizione una serie di classe ben organizzate. Il punto di partenza è ciò che si vuole stampare, rappresentato da una classe NSView o una sua sottoclasse. Nei casi più semplici, basta inviare alla view il messaggio print:, e la cosa finisce lì.

figura 01

figura 01

Però, buone operazioni di stampa richiedono due passaggi intermedi: l'impostazione del formato di stampa, e i parametri della stampa vera e propria. L'impostazione del formato è quel simpatico pannello che esce fuori quando si seleziona da menu la voce Formato di Stampa o Page Setup. Con questo pannello, l'utente determina quale stampante utilizzare, le dimensioni della pagina, l'orientamento della carta, cose del genere.

figura 02

figura 02

I parametri della stampa si impostano invece con il pannello che esce quando si seleziona la voce di menu Stampa o Print; tra i parametri, il numero di copie, caratteri specifici della stampante utilizzata (qualità di stampa, ad esempio), l'eventuale produzione di un file PDF al posto della stampa su carta, eccetera.

Cocoa non distingue la provenienza delle informazioni, se dal Formato di Stampa o dal pannello di Stampa, ma conserva tutte le informazioni necessarie utilizzando un oggetto della classe NSPrintInfo. Di questo oggetto, ne esiste una istanza condivisa con valori di default, ma potrebbe essere necessario produrne una copia per ogni tipologia di stampa desiderata.

Per visualizzare il pannello Formato di stampa, Cocoa mette a disposizione la classe NSPageLayout, mentre il pannello di stampa è gestito tramite la classe NSPrintPanel. Quando si lancia una stampa, l'operazione è gestita da un oggetto NSPrintOperation, che interagisce con un oggetto NSPrinter contenente una descrizione astratta della stampante.

Per realizzare le operazioni di stampa, occorre rispondere ai messaggi print: (se si è una view) o meglio printDocument: per la stampa di documenti (dentro la classe NSDocument). Il mio caso è il secondo, anzi, è più complicato, perché non solo l'applicazione è in grado di gestire documenti, ma dovrà lanciare due procedure di stampa completamente differenti: una per la stampa della copertina, una per la stampa del catalogo.

Formato di stampa

Il pannello Formato di Stampa è gestito dalla classe NSDocument o NSApplication (a seconda se l'applicazione dispone di documenti o no). Normalmente non occorre scrivere codice particolare, in quanto la gestione di default va spesso bene. Ovviamente, voglio provare a fare qualcosa di strano: scopro che la realizzazione standard non permette l'impostazione di margini (a meno di non definire un formato di pagina proprietario). Mi accingo quindi a particolarizzare il pannello con una vista che permetta di impostare i margini di stampa nel caso in cui l'oggetto da stampare sia una copertina (c'è una ragione: la parte frontale di un jewel box è un quadrato di dodici centimetri; ripiegato, si ha una copertina di dodici per ventiquattro centimetri, che non si riesce a stampare in un foglio A4 con i margini standard di un pollice ed un quarto).

Il metodo chiamato è runPageLayout, e va sovrascritto all'interno della mia classe NSDocument, ovvero CdCatDoc.

- (void)
runPageLayout:(id)sender
{
    CoverView * cvrView = [AppDelegate getTheDrawView ] ;
    // se ho davanti una CoverView, specializzo la stampa
    if ( cvrView )
    {
        [ cvrView managePageLayout ] ;
        return ;
    }
    // per il momento, gli altri casi usano la tecnica standard
    [ super runPageLayout: sender ] ;
}

Ricordo che due sono le possibili finestre: la copertina ed il catalogo. Quindi, per prima cosa, mi chiedo quale sia la finestra di fronte a tutte le altre; se si tratta di una CoverView, la faccio lavorare; altrimenti, utilizzo il meccanismo standard.

figura 03

figura 03

Bisogna quindi andare all'interno della classe CoverView e scrivere il metodo da me invocato. Tuttavia, prima c'è da risolvere un problema. La documentazione dice: potete aggiungere al pannello Formato di Stampa un vostro pannello, purché si tratti di una NSView, da aggiungere col metodo setAccessoryView. A lungo mi sono domandato come costruire questa view (l'opzione di definire tutti gli elementi tramite istruzioni Cocoa mi sembrava veramente macchinosa), quando ho poi realizzato che potevo fare tutto da Interface Builder. Qui ho costruito una finestra minimale, con quattro campi dove modificare i margini di stampa, e la scritta informativa sulle unità di misura in uso. Il tutto è completato dalal classe controllore PrtMrgWinCtl. Ora però, una finestra (NSWindow) non è una NSView (che invece mi serve); ma il trucco sta nell'utilizzare la view interna della finestra. Ecco il codice:

- (void)
managePageLayout
{
    NSPageLayout    * pageLayout = [NSPageLayout pageLayout];
    PrtMrgWinCtl    * prtWinCtl ;
    // carico la vista accessoria con l'impostazione dei margini
    prtWinCtl = [ [ PrtMrgWinCtl alloc] initWithWindowNibName: @"printMargin" ];
    [ prtWinCtl setRefCvrView: self ] ;
    [ pageLayout setAccessoryView: [ [ prtWinCtl window ] contentView ] ];
    // recupero i margini dall'istanza corrente e li assegno alla vista
    [ prtWinCtl setUpMargins: NSMakeRect(
        [ pInfo topMargin ], [ pInfo leftMargin ],
        [ pInfo rightMargin ], [ pInfo bottomMargin ]) ];
    // faccio girare l'ambaradan
    [ pageLayout beginSheetWithPrintInfo: pInfo
        modalForWindow: [ winCtl window ]
        delegate: self
        didEndSelector: @selector(pageLayoutDidEnd:returnCode:contextInfo:)
        contextInfo: nil ] ;
    // eventuali modifiche ai margini sono gestiti direttamente da PrtMrgWinCtl
}

Recupero inizialmente l'istanza condivisa di NSPageLayout; carico poi la finestra che ho costruito in Interface Builder, ed assegno a NSPageLayout la contentView della finestra stessa. Poi utilizzo la classe PrtMrgWinCtl per impostare il contenuto dai quattro campi con i valori di default. Questi li estraggo dalla nuova variabile d'istanza pInfo, che mi sono premunito di inizializzare (all'interno del metodo initWithFrame: della CoverView) con i valori forniti dall'istanza condivisa di NSPrintInfo, utilizzando la semplice istruzione seguente:

[ self setPInfo: [[NSPrintInfo sharedPrintInfo] copy] ];

Il metodo setUpMargin è di una facilità estrema, visto come sono andate le cose il capitolo precedente:

- (void)
setUpMargins: (NSRect) durRec
{
    int        cu = [refCvrView curUnits ] ;
    float    tmpVal ;
    [ curUnits setStringValue: stringForUnits(cu) ];
    tmpVal = convertMeasure( durRec.origin.x, UNITS_PIXELS, cu );
    [ topMargin setFloatValue: tmpVal ];
    tmpVal = convertMeasure( durRec.origin.y, UNITS_PIXELS, cu );
    [ leftMargin setFloatValue: tmpVal ];
    tmpVal = convertMeasure( durRec.size.width, UNITS_PIXELS, cu );
    [ rightMargin setFloatValue: tmpVal ];
    tmpVal = convertMeasure( durRec.size.height, UNITS_PIXELS, cu );
    [ bottomMargin setFloatValue: tmpVal ];
}

Qui topMargin, leftMargin, eccetera sono gli outlet verso i campi di testo.

figura 04

figura 04

Tornando al metodo managePageLayout, questo è completato da una istruzione che fa partire la visualizzazione del pannello Formato di Stampa, comprensivo della mia nuova vista.

Cosa succede se l'utente cambia i margini? Semplicemente, è chiamato il metodo updateMargins della classe PrtMrgWinCtl, che mi ero premunito di stabilire come action in risposta ad ogni operazione sui campi di testo.

- (IBAction)
updateMargins:(id)sender
{
    // recupero la informazioni di stampa
    int        cu = [refCvrView curUnits ] ;
    NSPrintInfo    * pi = [ refCvrView pInfo ] ;
    float    x1, x2, x3, x4 ;
    x1 = convertMeasure( [ topMargin floatValue ], cu, UNITS_PIXELS) ;
    x2 = convertMeasure( [ leftMargin floatValue ], cu, UNITS_PIXELS) ;
    x3 = convertMeasure( [ rightMargin floatValue ], cu, UNITS_PIXELS) ;
    x4 = convertMeasure( [ bottomMargin floatValue ], cu, UNITS_PIXELS) ;
    [ pi setTopMargin: x1 ] ;
    [ pi setLeftMargin: x1 ] ;
    [ pi setRightMargin: x1 ] ;
    [ pi setBottomMargin: x1 ] ;
}

Il metodo non fa altro che recuperare i valori ed impostarli all'interno dell'istanza NSPrintInfo propria della CoverView.

Per vedere come utilizzare questi valori, ho ad esempio verificato, prima della stamap effettiva, che la CoverView sia contenuta per interno all'interno della pagina. Infatti, all'interno di managePageLayout, l'ultima istruzione che fa partire la visualizzazione del pannello, stabilisce anche che debba essere chiamato il metodo pageLayoutDidEnd:returnCode:contextInfo: al termine delle operazioni; normalmente, questa oeprazione non è necessaria, ma io ho voluto aggiungere appunto un controllo:

- (void)
pageLayoutDidEnd: (NSPageLayout *)pageLayout
    returnCode:(int)returnCode
    contextInfo:(void *)contextInfo
{
    [ self checkPageDims ];
}

Il metodo che verifica la congruenza delle dimensioni è piuttosto lungo ma tutto sommato semplice. Le istruzioni fondamentali per il controllo sono le prime, tutto il resto è solo amichevolezza verso l'utente.

- (void)
checkPageDims
{

    float        printableWidth, printableHeight, scaleFactor, tmp ;
    // tengo conto del fattore di scala
    scaleFactor = [[[pInfo dictionary] objectForKey:NSPrintScalingFactor] floatValue];
    tmp = curViewSize.width * scaleFactor ;
    printableWidth = [pInfo paperSize].width - tmp
                    - ([pInfo leftMargin] + [pInfo rightMargin]);
    tmp = curViewSize.height * scaleFactor ;
    printableHeight = [pInfo paperSize].height - tmp
                    - ([pInfo topMargin]+[pInfo bottomMargin]);
    // potremmo avere delle dimensioni troppo piccole
    if ( printableWidth < 0 || printableHeight < 0 )
    {
        // costruisco un alert
        NSAlert *alert = [[NSAlert alloc] init];
        NSString    * messaggio = [ NSString string ];
        // e' un warning, che tanto si puo' andare avanti
        [alert setAlertStyle:NSWarningAlertStyle];    
        // c'e' un solo pulsante sdi ok
        [alert addButtonWithTitle:@"OK"];
        // dico che la copertina non ci sta nella pagina
        [alert setMessageText:@"The cover will not fit into the page"];
        // e dico anche per quanto
        if ( printableWidth < 0 )
        {
            float    tmpVal1, tmpVal2 ;
            tmpVal1 = convertMeasure( -printableWidth, UNITS_PIXELS, curUnits );
            tmpVal2 = convertMeasure( curViewSize.width, UNITS_PIXELS, curUnits );
            messaggio = [ messaggio stringByAppendingFormat:
                @"Required width: %7.3f (%@), missing %7.3f\n",
                tmpVal2, stringForUnits(curUnits), tmpVal1 ] ;
        }
        if ( printableHeight < 0 )
        {
            float    tmpVal1, tmpVal2 ;
            tmpVal1 = convertMeasure( -printableHeight, UNITS_PIXELS, curUnits );
            tmpVal2 = convertMeasure( curViewSize.height, UNITS_PIXELS, curUnits );
            messaggio = [ messaggio stringByAppendingFormat:
                @"Required height: %7.3f (%@), missing %7.3f",
                tmpVal2, stringForUnits(curUnits), tmpVal1 ] ;
        }
        [ alert setInformativeText: messaggio ];
        [ alert runModal ];
    }
}

figura 05

figura 05

Dalla variabile pInfo ricavo le dimensioni della pagina, i margini, ed il fattore di scala; dalla CoverView ricavo invece le sue dimensioni, che correggo tenedo conto del fattore di scala (quello che si può impostare in sede di Formato di Stampa, non si tratta del fattore di zoom della finestra). Se le dimensioni della copertina superano quelle disponibili per la stampa (le dimensioni della pagina cui sono sottratti i margini), allora mostro un messaggio di avvertimento (un NSAlert), opportunamente costruito: un solo tasto di Ok, il messaggio che dice che la copertina non rientra nella pagina, e perfino un testo che spiega quanto spazio manca per una corretta stampa.

Tutto ciò conclude l'argomento sul Formato di Stampa. Adesso provo l'effettiva stampa.

Stampa di una copertina

In una applicazione basata su documenti, il menu Stampa normalmente richiede l'esecuzione del metodo printDocument alla classe NSDocument (attenzione: ho dovuto cambiare la voce di menu Print del file MainMenu.nib perché, all'inizio dei tempi, l'avevo associata al metodo print:; per lo stesso motivo, all'interno di ListWinCtl rimuovo un metodo che associava lo stesso metodo ad un elemento della toolbar). L'oggetto documento associato (nel mio caso, CdCatDoc) riceve il messaggio e chiama a sua volta il metodo printShowingPrintPanel:. Ora, l'oggetto documento standard NSDocument non conosce la rappresentazione che del documento viene fatta, nel caso particolare; in altre parole, non conosce quale sia la View da stampare, e non può quindi fornire un metodo di default per la stampa. In definitiva, devo sovrascrivere il metodo printShowingPrintPanel per far funzionare le cose.

- (void)
printShowingPrintPanel:(BOOL)showPanels
{
    CoverView * cvrView = [AppDelegate getTheDrawView ] ;
    NSOutlineView * outView = [AppDelegate getTheOutlineView ] ;
    if ( cvrView )
    {
        [ cvrView managePrintWithPanels: showPanels ] ;
        return ;
    }
    if ( outView )
    {
        ...
    }
}

Per quando riguarda la stampa della copertina, mi sbrigo subito passando il compito, ancora una volta, alla CoverView. Il parametro showPanels indica se bisogna o meno visualizzare il pannello di stampa.

Tornando quindi all'interno della classe CoverView, ecco il metodo principale per la stampa.

- (void)
managePrintWithPanels:( BOOL ) showPanels
{
    NSPrintOperation    * po ;
    // verifico se ci sta nella pagina
    [ self checkPageDims ];
    // comincio le operazioni di stampa
    po = [ NSPrintOperation printOperationWithView: self printInfo: pInfo ];
    [ po setShowPanels: showPanels ];
    [ po runOperationModalForWindow: [ winCtl window]
        delegate: nil didRunSelector:nil contextInfo: nil ];
}

La prima istruzione verifica (sono paranoico, lo so) che la copertina stia all'interno della pagina; quindi, costruisco un oggetto della classe NSPrintOperation, destinato a controllare le operazioni di stampa. Questo oggetto è inizializzato con due parametri: la vista che deve essere stampata (facile, la CoverView stessa) e le informazioni di stamap (le ho conservate in pInfo). Poi, ignorando bellamente il parametro showPanels, mostro sempre e comunque il pannello di stampa, come uno sheet per la finestra che contiene la copertina.

Fine. Non c'è altro da fare.

Non è vero, ci sono alcuni piccoli accorgimenti per ottenere un buon risultato; in effetti, la rappresentazione a video e su carta potrebbero essere leggermente differenti in qualche caso. Ad esempio, se la CoverView visualizza la griglia, non è bello che la griglia risulti stampata. Ancora, non è bello che ci siano elementi selezionati, che comparirebbero gli inestetici handle anche sulla carta.

Per migliorare dunque la stampa, occorre che i metodi drawRect (che sono indifferentemente utilizzati per video e stampa) conoscano il contesto in cui si trovano a disegnare. Basta infatti chiedersi con l'istruzione seguente:

[NSGraphicsContext currentContextDrawingToScreen]

se il disegno avviene sullo schermo (il risultato dell'istruzione è YES) oppure NO (e per ora l'unica altra possibilità è la stampa).

Quindi modifico il metodo drawRect: di CoverView nella seguente maniera:

- (void)
drawRect:(NSRect)rect
{
    BOOL    scrOrPrt = [NSGraphicsContext currentContextDrawingToScreen] ;
    // sfondo bianco
    if ( scrOrPrt )
    {
        [[ NSColor whiteColor] set ];
        NSRectFill( rect );
    }
    // se c'e' la griglia, la disegno
    if ( scrOrPrt && showGrid)
        [ self drawGrid: rect ];
    // poi dico di disegnarsi a tutti gli elementi
    [ theElements drawElement: rect ];
    // e poi all'elemento in corso
    if ( scrOrPrt && curCreatingElem)
    {
        [ curCreatingElem drawElement: rect ] ;
    }
    if ( scrOrPrt && curSelectRect)
    {
        NSBezierPath    * curPath = [ curSelectRect theDrawPath ];
        float            strokePattern[2] ={ 5, 5 } ;
        [ curPath setLineDash: strokePattern count: 2 phase: 0.0];
        [ curSelectRect setTheDrawPath: curPath ];
        [ curSelectRect drawElement: rect ] ;
    }
}

Con la variabile srcOrPrt evito di stampare lo sfondo bianco (che sullo schermo serve a coprire eventuali rimasugli di pixel), la griglia, ed evito anche di disegnare (ma non dovrebbe mai succedere) i rettangoli di selezione corrente e l'elemento in corso di costruzione.

All'interno delle classi CCE_BasicForm utilizzo una forma diversa della stessa istruzione per condizionare il disegno degli handle, ma il succo della faccenda non cambia.

- (void)
drawElement: (NSRect) inRect
{
    // per salvare il contesto
    NSGraphicsContext *context = [NSGraphicsContext currentContext];
    [context saveGraphicsState];
    [ localTF concat ];
    [ self specificDrawing: inRect ];
    if ( [context isDrawingToScreen ] )
        [ self drawHandles ] ;
    // ripristino del contesto
    [context restoreGraphicsState];
}

Con queste poche istruzioni, ho prodotto una buona stampa della copertina. Ci sono riuscito con pochi sforzi, e pensavo che altrettanti pochi sforzi servissero per stampare il catalogo. Raramente mi sono sbagliato così tanto.

Stampa del catalogo

Se ci si volesse limitare ad una stampa di base, non ci sono problemi. Già ho stabilito che non mi interessa manipolare ulteriormente il pannello Formato di Stampa, che lascio gestire in maniera standard dal metodo runPageLayout. Potrei cavarmela facilmente facendo qualcosa di simile al metodo managePrintWithPanels, dove però si utilizza la NSOutlineView come view da stampare. In effetti, in questo modo, si ottengono dei risultati appena sufficienti.

- (void)
printShowingPrintPanel:(BOOL)showPanels
{
    CoverView * cvrView = [AppDelegate getTheDrawView ] ;
    NSOutlineView * outView = [AppDelegate getTheOutlineView ] ;
    if ( cvrView )
    {
        ...
    }
    // se sto stampando il catalogo, faccio qualche correzione
    // alle impostazioni di stampa
    if ( outView )
    {
        NSPrintOperation    * po ;
        // comincio le operazioni di stampa
        po = [ NSPrintOperation printOperationWithView: outView printInfo: [ self printInfo ] ];
        [ po setShowPanels: showPanels ];
        [ po runOperationModalForWindow: [ outView window]
            delegate: nil didRunSelector:nil contextInfo: nil ];
    }
}

figura 06

figura 06

Ci sono diversi punti sui quali lavorare: il primo è di non passare da una pagina alla successiva nel bel mezzo di una riga, ma di fare in modo che si passi alla pagina successiva solo in corrispondenza di righe complete. Un secondo punto è la necessità di trasformare la outlineView in maniera acconcia (è molto brutto stampare una riga a sfondo bianco ed una azzurra). Il terzo punto è la possibilità di inserire intestazioni e piè di pagina, se non altro per indicare il numero di pagina corrente. Bene, si tratterà di sovrascrivere qualche metodo.

Sono pochi, e a prima vista molto chiari:

- (BOOL)    knowsPageRange:(NSRangePointer)range ;
- (NSRect)    rectForPage:(int)page ;
- (void)    drawPageBorderWithSize:(NSSize)borderSize;

I primi due sono necessari, mentre il terzo è opzionale. Il primo metodo è chiamato una sola volta, all'inizio dei tempi; la realizzazione di default restituisce NO per lasciare fare tutto a Cocoa come preferisce. Per sovrascriverlo occorre restituire YES ed impostare il numero delle pagine che saranno stampate. Poi, per ogni pagina, è chiamato il secondo metodo; gli viene fornito il numero di pagina, e deve restituire un rettangolo; questo rettangolo è poi utilizzato come argomento del metodo drawRect: della vista da stampare. Se la vista è ad esempio alta 1000 pixel e la pagina ha una altezza di 400 pixel, im emtodo deve produrre in successione tre rettangoli:

Rettangolo 1: x: 0, y: 0, w: <larghezza>, h: 400
Rettangolo 2: x: 0, y: 400, w: <larghezza>, h: 400
Rettangolo 3: x: 0, y: 800, w: <larghezza>, h: 200

Infine, il terzo metodo è chiamato poco prima di disegnare una pagina, e permette di inserire elementi grafici accessori, come appunto i numeri di pagina, intestazioni e piè di pagina.

Il problema fondamentale di questo meccanismo è che questi tre metodi sono propri di elementi della classe NSView o discendenti; quindi, NSOutlineView ha una sua realizzazione standard, che dovrei sovrascrivere in qualche modo. Qui sono cominciati i problemi; ho penato parecchio prima di arrivare ad una soluzione vedibile (o meglio, stampabile), ricorrendo a diverse strategie e a diversi meccanismi, che mi hanno portato via un sacco di tempo. Alla fine, tra tutti i metodi che ho provato con scarsi risultati, sono pervenuto a quello che descrivo qui di seguito, che mi pare concettualmente il più semplice e flessibile, ma che continua ad avere i suoi (a volte devastanti) problemi.

L'idea è di non stampare direttamente la NSOutlineView, ma una vista differente, costruita al volo proprio per la stampa. Questa vista sarà in realtà minimale, solo un pretesto per avere una classe del tipo NSView per la quale sovrascrivere i metodi citati (ed anche qualcun altro).

Chiamo questa nuova classe PrintView, e per il momento gli fornisco una sola variabile d'istanza v2p, un oggetto NSOutlineView appunto da stampare. Di più, per inizializzare la classe richiedo proprio tale oggetto:

- (id)
initWithOutline:(NSOutlineView *) outView
{
    v2p = outView ;
    ...
    // metto un frame a caso, sara' aggiustato poi
    self = [super initWithFrame: [ outView frame ] ];
    return self;
}

Mi disinteresso anche del frame della vista, che tanto sarà aggiustato successivamente.

In base a queste premesse, il metodo printShowingPrintPanel di CdCatDoc diventa:

- (void)
printShowingPrintPanel:(BOOL)showPanels
{
    CoverView * cvrView = [AppDelegate getTheDrawView ] ;
    NSOutlineView * outView = [AppDelegate getTheOutlineView ] ;
    if ( cvrView )
        ...
    // se sto stampando il catalogo, faccio qualche correzione
    // alle impostazioni di stampa
    if ( outView )
    {
        PrintView            * pw ;
        NSPrintOperation    * po ;
        NSPrintInfo            * pi = [ self printInfo ] ;
        // dico che la stampa avviene da in alto a sinistra
        // (il default a quanto pare e' al centro)
        [ pi setHorizontallyCentered: NO ] ;
        [ pi setVerticallyCentered: NO ] ;
        pw = [ [ PrintView alloc] initWithOutline: outView ];
        // comincio le operazioni di stampa
        po = [ NSPrintOperation printOperationWithView: pw printInfo: pi ];
        [ po setShowPanels: showPanels ];
        [ po runOperationModalForWindow: [ outView window]
            delegate: nil didRunSelector: nil contextInfo: nil ];
    }
}

Rispetto alla realizzazione precedente, c'è appunto la costruzione di una PrintView, e una caratterizzazione delle printInfo; a quanto pare le opzioni standard di stampa cercano di centrare la vista stampata sia in orizzontale che in verticale; per quanto mi riguarda, la stampa invece dovrà partire in alto a sinistra.

A questo punto, è tutto compito della classe PrintView.

Pagine e pagine

Il primo metodo chiamato è knowsPageRange, da sovrascrivere se si vuole un meccanismo di paginazione proprietario. Eccolo:

- (BOOL)
knowsPageRange:(NSRangePointer)range
{
    [ self prepareViewForPrinting: YES ] ;
    [ self calcPrintValues ] ;
    range->location = 1;
    range->length = totPageNum ;
    return YES;
}

Il grosso del lavoro è svolto da due metodi accessori, che predispongono una serie di variabili d'istanza, tra le quali totPageNum, che contiene appunto il numero totale delle pagine da stampare.

Con il metodo prepareViewForPrinting predispongo la NSOutlineView affinché sia stampata in maniera acconcia; userò lo stesso metodo con diverso parametro per ripristinare lo stato precedente (userò il metodo endDocument, chiamato da Cocoa al termine delle operazioni di stampa).

- (void )
prepareViewForPrinting: (BOOL) toPrint
{
    if ( toPrint )
    {
        // blocco l'aggiornamento della finestra
        [ [ v2p window] setAutodisplay: NO ] ;
        // adatto la vista ad una situazioen di stampa
        setupColumn( v2p, FALSE, COLID_ADD2PRINT );
        [ v2p setUsesAlternatingRowBackgroundColors: NO ] ;
        [ v2p setGridStyleMask:
                (NSTableViewSolidVerticalGridLineMask |
                NSTableViewSolidHorizontalGridLineMask ) ] ;
    }
    else
    {
        // rispristino la situazione precedente
        setupColumn( v2p, TRUE, COLID_ADD2PRINT );
        [ v2p setUsesAlternatingRowBackgroundColors: YES ] ;
        [ v2p setGridStyleMask: NSTableViewSolidVerticalGridLineMask ] ;
        [ [ v2p window] setAutodisplay: YES ] ;
    }
}

Le modifiche sono sostanzialmente tre (potrei aggiungerne altre, ma mi bastano queste come prova concettuale della fattibilità): faccio scomparire la colonna is2Print (brutta a vedersi in stampa, ed anche poco significativa); le righe hanno sfondo uniforme; in stampa ho la griglia piena (in visualizzazione, avendo righe a colore alternato, le righe della griglia sono solo quelle verticali).

Molto più denso di concetto l'altro metodo, dove sono svolte operazioni fondamentali per l'intero processo di stampa. La spiegazione di questo metodo sarà molto più lunga del metodo stesso.

- (void)
calcPrintValues
{
    NSSize            dimPagStampa ;
    // aggiusto le dimensioni della outlineView per
    // comprendere solo fino all'ultima colonna utile
    NSRect            cr = [ v2p rectOfColumn: ([ v2p numberOfColumns] - 1 ) ] ;
    float            larghOutline = cr.origin.x + cr.size.width ;
    // Obtain the print info object for the current operation
    NSPrintInfo *pi = [[NSPrintOperation currentOperation] printInfo];
    // Calculate the page height in points
    NSSize paperSize = [pi paperSize];
    float pageHeight = paperSize.height - [pi topMargin] - [pi bottomMargin];
    float pageWidth = paperSize.width - [pi leftMargin] - [pi rightMargin];
    pageMargins = NSMakeRect( [pi leftMargin], [pi topMargin],
                             [pi rightMargin], [pi bottomMargin] );
    // Convert height to the scaled view
    scaleFactor = [[[pi dictionary] objectForKey:NSPrintScalingFactor]
                    floatValue];
    dimPagStampa.height = pageHeight / scaleFactor; ;
    dimPagStampa.width = pageWidth / scaleFactor;
    
    // scelgo come larghezza la minima tra stamap e outline
    if ( dimPagStampa.width <= larghOutline )
            larghezzaPagina = dimPagStampa.width ;
    else    larghezzaPagina = larghOutline ;
    // calcolo quando spazio occupa in verticale una riga
    altezzaRiga = [ v2p rowHeight ] + [ v2p intercellSpacing ].height ;
    // calcolo quante righe ci stanno in una pagina
    // tengo conto del fattore di scala...
    righePerPagina = dimPagStampa.height / altezzaRiga ;
    // nuovo valore per l'ampiezza pagina
    altezzaPagina = righePerPagina * altezzaRiga ;
    // e per il margine inferiore (un po' piu' grande)
    pageMargins.size.height += (dimPagStampa.height - altezzaPagina) * scaleFactor ;
    // calcolo quante sono le righe totali da stamapre
    righeDaStampare = [ v2p numberOfRows ] ;
    // basta adesso fare una divisione intera
    totPageNum = righeDaStampare / righePerPagina + 1 ;
}

figura 07

figura 07

Una delle prime operazioni svolte è il calcolo della variabile larghOutline; lo faccio in questo modo strano perché non sempre la dimensione orizzontale della NSOutlineView corrisponde all'area effettiva di stampa. Ad esempio, se la dimensione complessiva delle colonne è inferiore alla dimensione della finestra (come nella figura qui vicino), la dimensione della NSOutlineView è pari alla dimensione di tutta la finestra, mentre la zona da stampare riguarda solo le colonne effettivamente visibili.

Sono poi calcolate alcune variabili dipendenti dalla pagina scelta per la stampa; in particolare sono conservati in una variabile d'istanza (NSRect, ma solo perché è comoda per conservare quattro valori) i margini della pagina (che qui sono sempre quelli di default: un pollice a destra e sinistra, un pollice ed un quarta sopra e sotto), e la dimensione complessiva della pagina in stampa. E qui entra in ballo il fattore di scala (quello impostato con il pannello Formato di Stampa). Potrei tediarvi per ore con le mie vicissitudini sull'impatto del fattore di scala nelle mie varie realizzazioni, ma ho pietà dei miei venticinque lettori; qui mi limito a raccontare l'ultimo modello concettuale che mi aiuta nella progettazione.

Quando ho un fattore di scala diverso da 1 (ovvero, diverso dal 100%), significa che devo scalare il disegno in maniera opportuna; oppure, ed è qui l'idea, posso disegnare come se niente fosse, ma ho una pagina che è più grande o più piccola in dipendenza del fattore di scala. Se ad esempio il fattore di scala è 50%, è come se mi trovassi a disegnare su una pagina larga e lunga il doppio. Viceversa, con un fattore di scala del 200%, è come lavorare su un pagina metà larga e lunga della dimensione standard. In base a queste considerazioni, alla variabile d'istanza dimPagStampa sono assegnati i valori calcolati direttamente dall'oggetto printInfo, opportunamente moltiplicati (divisi, in realtà) per il fattore di scala. L'influenza del fattore di scala sarà d'ora in poi pervasiva.

La variabile dimPagStampa descrive l'area di stampa disponibile sulla pagina. Ma in realtà la stampa effettiva occupa una dimensione differente: la NSOutlineView potrebbe ad esempio essere più piccola, e non occupare tutta la larghezza. È quindi calcolata la variabile d'istanza larghezzaPagina, che appunto contiene quanto spazio in orizzontale è occupato. Se la NSOutlineView è molto larga, limito l'area di stampa a quella effettivamente disponibile; se invece è piuttosto stretta, uso la sua larghezza come limite.

Ben più importante è invece il calcolo della variabile altezzaPagina. Qui c'è il problema di avere una altezza multipla esatta dell'altezza di una riga, per evitare che nella stampa una riga si trovi a cavallo tra due pagine. Calcolata in altezzaRiga l'altezza effettiva della riga (volete che vi racconti quanto ho penato prima di capire l'influenza di intercellSpacing?), in righePerPagina il numero di righe possibili in una pagina (righePerPagina è un numero intero, quindi, la divisione tra l'altezza dell'area di stampa e l'altezza di una riga è sempre troncato all'intero immediatamente più piccolo), si ottiene facilmente il valore altezzaPagina moltiplicando i due precedenti valori. Ed adesso, attenzione al gioco di prestigio (fondamentale in un successivo momento); visto che ho cambiato l'altezza della pagina, il margine inferiore non è più quello giusto, ma lo devo correggere proprio della differenza tra l'altezza dell'area di stampa iniziale e altezzaPagina. Questa differenza però fa riferimento a dimensioni già scalate (lo è l'area di stampa), mentre i margini non sono scalati; ecco perché lo moltiplico per il fattore di scala (ed anche questa moltiplicazione è stata un bagno di sangue, prima di capirla).

Coraggio, siamo quasi alla fine; adesso è estremamente facile calcolare il numero delle pagine; trovo quante sono le righe da stampare (tutte quelle della NSOutlineView), e le divido per il numero di righe in una pagina; devo aggiungere uno per avere la pagina finale, quella che contiene normalmente un numero di righe inferiore al massimo. Questo è il valore che assegno a totPageNum.

Il rettangolo della pagina

Col metodo precedente ho calcolato il numero di pagine necessarie; adesso, per ogni pagina, devo specificare il rettangolo all'interno del quale avviene la stampa. Il metodo è il citato rectForPage; la realizzazione è la seguente, e me ne vergogno un po':

- (NSRect)
rectForPage:(int)page
{
    NSRect    lr ;
    // offset corrente
    float    dimpage = (page - 1) * altezzaPagina ;
    // righe ancora da stampare
    int        restoRighe = righeDaStampare - (page-1)* righePerPagina ;
    // la dimensioen della pagina e' per il momento quella standard
    curAltezzaPagina = altezzaPagina ;
    // se pero' ci sono poche righe rimaste...
    if ( restoRighe < righePerPagina )
            curAltezzaPagina = restoRighe * altezzaRiga ;
    // un trucco per tracciare comunque una pagina
    if ( curAltezzaPagina < 1 )    curAltezzaPagina = 1 ;
    // ecco la pagina da stampare
    lr = NSMakeRect( 0, dimpage , larghezzaPagina, curAltezzaPagina ) ;
    // una strana porcheria per correggere le stampe ingrandite
    if ( scaleFactor > 1 )
    {
        lr.size.height *= scaleFactor ;
        lr.size.width *= scaleFactor ;
    }
    curPageNum = page ;
    return ( lr );
}

Questo metodo deve produrre un rettangolo, generalmente di dimensioni fisse (nel caso, larghezzaPagina e altezzaPagina), ma con il punto di origine che si sposta di pagina in pagina per spazzolare la vista in stampa. Si può immaginare la vista in stampa come una grande rettangolo, mentre il rettangolo prodotto da questo metodo parte dal punto in alto a sinistra e si muove verso il basso, spostandosi di volta in volta della dimensione di un'area di stampa, come un oblò che mette in evidenza la porzione della vista principale da stampare.

Ed in effetti le prime istruzioni vanno in questa direzione: nota altezzaPagina, e il numero di pagina (che parte da 1), posso facilmente calcolare dimpage come l'origine del rettangolo. Ci sono poi alcune istruzioni per aggiustare il valore di altezzaPagina per tenere conto dell'ultima pagina (che presente in genere un numero di righe inferiore al massimo consentito) o di una pagina vuota (se restituissi un rettangolo vuoto, la pagina non sarebbe del tutto stampata; decido invece di stampare comunque una pagina vuota imponendo una dimensione minima di un pixel).

Per tentativi ed errori sono arrivato alla prima porcheria del capitolo (ne troverete altre). Per qualche strano motivo, quando il fattore di scala è superiore al 100% (quindi, in caso di ingrandimenti), il rettangolo prodotto (e poi fornito al metodo drawRect:) è troppo piccolo, e provoca un errato clipping della pagina stampata. Con la correzione sulle dimensioni del rettangolo, le cose funzionano correttamente. Ma non chiedete perché.

A questo punto, per disegnare una pagina (lascio per il momento perdere intestazione e piè di pagina, li vedrò più tardi), basta scrivere il metodo di disegno della vista:

- (void)
drawRect: (NSRect) rect
{
    // c'e' solo la outline da disegnare...
    [ v2p drawRect: rect ] ;
}

Estremamente semplice ed ovvio, e sbagliato.

Ancora una volta, ho penato tantissimo per venirne fuori.

Tutto in effetti funziona finché si stampa al 100%. Non appena ho un fattore di scala differente, si nota che in realtà la NSOutlineView non viene scalata correttamente. Ho impiegato parecchie ore prima di capire che non è attiva la trasformazione di scala. La cosa triste è che usando la paginazione standard (in pratica, non sovrascrivendo knowsPageRange e rectForPage), la trasformazione di scala è attiva, mentre, semplicemente sovrascrivendoli, non lo è più. Chissa perché.

Provo a riscrivere il tutto così, realizzando esplicitamente la scalatura:

- (void)
drawRect: (NSRect) rect
{
    NSAffineTransform    * tf = [ NSAffineTransform transform ] ;
    [ tf scaleBy: scaleFactor ] ;
    [ tf concat ] ;
    // c'e' solo la outline da disegnare...
    [ v2p drawRect: rect ] ;
}

Ottengo risultati bizzarri: porzioni della NSOutlineView non sono stampati, o sono stampati in posti completamente sbagliati, come se fossero spostati in altri posti. Ancora una volta , ho penato molto, confrontando le situazioni in paginazione standard e paginazione proprietaria. Sembra che la trasformazione per il cambio del sistema di riferimento tra pagina e area di stampa non funzioni correttamente. Dopo aver a lungo navigato nella documentazione, ho trovato un metodo che sembra fare al caso mio: il metodo beginPageInRect: è chiamato per ogni pagina dopo rectForPage: ma prima di drawRect:, ed il suo compito principale è l'impostazione del contesto grafico (tra cui, appunto la trasformazione del sistema di riferimento). L'analogo metodo endPage (dopo il disegno della pagina) rispristina la situazione iniziale.

Colto dalla disperazione, mi sono accinto a calcolare da zero la trasformazione. Allora, il punto di partenza è il sistema di riferimento (SdR) della pagina. L'origine è in basso a sinistra, con asse delle ascisse verso destra e delle ordinate verso l'alto. Il SdR dell'area di stampa è però differente. La sua origine è in un punto alto a sinistra, distante dal lato sinistro e dall'alto quanto stabilito dai margini. L'asse delle ascisse è verso sinistra, le ordinate verso il basso. Inoltre, bisogna tenere conto dell'eventuale fattore di scala, che cambia le unità di misura. A tutto questo, occorre aggiungere il fatto che siamo all'ennesima pagina. In sintesi, ottengo il metodo successivo:

- (void)
beginPageInRect:(NSRect)aRect atPlacement:(NSPoint)location
{
    float                pageOffset ;
    NSAffineTransform    * tf = [ NSAffineTransform transform ] ;
    [ super beginPageInRect: aRect atPlacement: location ] ;
    // per qualche motivo, la trasformazione operata dalla superclasse
    // non va per nulla bene, mi tocca inventarne una da me
    pageOffset = curPageNum * altezzaPagina * scaleFactor ;
    // l'origine e' il punto in basso a sinistra della pagina
    // mi sposto in alto a sinistra, tenendo conto del margine
    [ tf translateXBy: pageMargins.origin.x
                 yBy: (pageOffset + pageMargins.size.height) ] ;
    // cambio la scala e rovescio l'asse delle y
    [ tf scaleXBy: scaleFactor yBy: -scaleFactor ] ;
    [ tf set ] ;
}

Ci sono alcuni ragionamenti da fare per tenere conto del fatto che le ordinate nei due SdR sono una opposta all'altra, e che quindi il punto in alto a sinistra va calcolato tenendo conto del margine inferiore (quello magicamente corretto in precedenza), al quale si aggiunge lo spazio della pagina (in unità reali, non quelle corrette dal fattore di scala). Mi spiace per voi, dovete ragionarci da soli, con qualche disegnino ed esempio, perché non riesco a spiegarmi meglio.

È importante notare l'istruzione di set piuttosto che concat (come finora applicato in tutte le trasformazioni affini); il messaggio set rimpiazza totalmente la trasformazione, mentre concat semplicemente l'aggiungeva a quella già presente.

Va da sé che, dal momento che la trasformazione è eseguita da questo metodo, il metodo drawView: mantiene la sua forma più semplice, con l'unica istruzione di disegno della NSOutlineView.

Infine, per permettermi e permettervi di capire meglio quanto sta succedendo, ho scoperto (non è stato banale) un metodo per conoscere la trasformazione corrente, raccolto nella seguente funzione presente in djZeroUtils.m:

void
printCTM(NSString* ctx )
{
    CGAffineTransform    xxx ;
    CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];
    xxx = CGContextGetCTM( myContext );
    NSLog(@"%@ x11: %8.3f, x12: %8.3f x21: %8.3f x22: %8.3f, tx: %8.3f, ty: %8.3f",
            ctx, xxx.a, xxx.b, xxx.c, xxx.d, xxx.tx, xxx.ty );
}

La funzione (che ha come argomento una stringa solo per bellezza) recupera il contesto grafico corrente, e da questo risale alla trasformazione attivo in quel contesto; ne stampo tutti i valori così da capire cosa sta succedendo.

Decorazioni

Concludo il capitolo su come si possono aggiungere ad una pagina intestazioni, piè di pagina ed in generale decorazioni di vario tipo, che si estendano anche su tutto lo spazio disponibile (al di fuori dell'area di stampa). Il metodo da utilizzare si chiama drawPageBorderWithSize, che normalmente fa nulla, e che quindi sovrascritto se si vuole qualcosa di carino. Al metodo è fornito un parametri NSSize che contiene le dimensioni della pagina.

La mia realizzazione è piuttosto lunga, ma semplice: voglio disegnare come piè di pagina una stringa che riporti il numero di pagina; come intestazione, riportare il nome del catalogo, e poi inserire sopra la stampa della NSOutlineView l'intestazione delle varie colonne, in pratica, disegnare la headerView della NSOutlineView. Infine, voglio contornare NSOutlineView e headerView con un bel rettangolo, giusto per dimostrare che sono capace di farlo.

Per capire come procede il disegno, ancora una volta si devono capire i SdR coinvolti. All'ingresso della funzione, il SdR attivo è quello della pagina (origine in basso a sinistra, eccetera). Poi si utilizza la coppia di messaggi lockFocus e unlockfocus per operare il trasferimento del SdR a quello della PrintView (in alto a sinistra, con ordinate rovesciate). Giocando sull'impostazione del frame, si può fare il modo che questo secondo SdR comprenda tutta la pagina o solo porzioni di essa.

La prima operazione è di conservare il frame corrente. Cosa ci sia dentro, non mi interessa; tuttavia, la documentazione di metterla da parte e di reimpostarla prima di terminare le operazioni. Successivamente, produco un percorso a forma di rettangolo e produco due stringhe; il tutto mi sarà utile poi.

- (void)
drawPageBorderWithSize:(NSSize)borderSize
{
    NSRect            circrect ;
    NSString        * str1, * str2 ;
    NSBezierPath    * cnt ;
    float            vertStrOff ;
    float            invSF = 1 / scaleFactor ;
    // qui e' gia' attiva la trasformazione di scala
    // metto da parte il frame corrente della vista
    NSRect            prevFrame = [ self frame ];
    // per tenere conto del fattore di scala, faccio finta di avere
    // a disposizione una pagina piu' grande...
    float            tm = pageMargins.origin.y * invSF;
    float            lm = pageMargins.origin.x * invSF;
    // una trasformazione per spostare i disegni delle viste
    NSAffineTransform    * tf = [ NSAffineTransform transform ] ;
    float        headHeight = [ [ v2p headerView ] frame ].size.height ;

    // un rettangolo che contiene tutta la pagina
    circrect = NSMakeRect(lm, tm - headHeight,
            larghezzaPagina, curAltezzaPagina + headHeight ) ;
    circrect = NSInsetRect( circrect, -1, -1) ;
    cnt = [NSBezierPath bezierPathWithRect: circrect ];
    // produco la stringa col numero di pagina
    str1 = [ NSString stringWithFormat: @"Page %d of %d",
                curPageNum, totPageNum ] ;
    str2 = [ NSString stringWithString: [[ v2p window] title ]] ;

Comicio adesso il primo blocco di disegno. Imposto come frame l'intera dimensione della pagina. Come al solito devo tenere conto del fattore di scala. Il successivo messaggio di lockFocus esegue la trasformazione: adesso l'origine del SdR è il vertice in alto a sinistra della pagina. Il rettangolo circrect è stato calcolato in base a questo SdR, e circonda l'intera area di stampa; in più; è stata aggiunto in testa, all'interno del margine superiore, uno spazio adatto a contenere la headerView. Disegno il rettangolo. Poi operO una trasformazione di scala. Infatti, voglio che le due stringhe compaiano sempre della stessa dimensione, indipendentemente dal fattore di scala.

Segue l'ennesima porcheria. Per qualche motivo che non capisco, a fattori di scala maggiori del 100%, la zona impostata con la prima istruzione di setFrame non comprende tutto lo spazio disponibile; mi tocca quindi cambiare il frame, allargandolo. L'oerazione si rende necessaria perché altrimenti la scritta col numero di pagina non compare.

    // primo blocco di disegno, imposto come frame tutta la pagina
    // (tengo conto della scala) per disegnare un bel contorno
    [ self setFrame: NSMakeRect( 0, 0, borderSize.width * invSF,
                                     borderSize.height * invSF ) ] ;
    // comincio il disegno del primo blocco
    [ self lockFocus ] ;
    // riempio giusto per vedere
    [[ NSColor blueColor] set ]; NSRectFill( circrect );
    // e traccio giusto per evidenziare
    [ [ NSColor redColor] set ]; [ cnt setLineWidth: 1 ]; [ cnt stroke ];
    // cambio la scala
    [ tf scaleBy: invSF ] ;
    [ tf concat ] ;
    // adesso il frame e' tutta la pagina (non chiedete...)
    [ self setFrame: NSMakeRect( 0, 0, borderSize.width, borderSize.height) ] ;
    // come footer il numero il pagina
    vertStrOff = pageMargins.origin.y + altezzaPagina * scaleFactor + 3 ;
    circrect = NSMakeRect(pageMargins.origin.x, vertStrOff,
                        larghezzaPagina * scaleFactor, 25 ) ;
    [ str1 drawInRect:circrect withAttributes: nil ] ;
    // come header, il nome del catalogo
    vertStrOff = pageMargins.origin.y - headHeight * scaleFactor - 20 ;
    circrect = NSMakeRect(pageMargins.origin.x, vertStrOff,
                        larghezzaPagina * scaleFactor, 25 ) ;
    [ str2 drawInRect:circrect withAttributes: nil ] ;
    // rispristino la scala per le successive operazioni
    [ tf scaleBy: scaleFactor ] ;
    // fine primo blocco di disegno
    [ self unlockFocus ] ;

Se il fattore di scala è molto grande, c'è il pericolo che la scritta in intestazione esca dalla pagina. A poco vale costringerla all'interno: il successivo disegno della headerView coprirebbe comunque la stringa.

Ad ogni modo, le istruzioni successive ripristina la trasformazione di scala ed eseguono un unlockFocus. L'operazione mi serve per cambiare ancora una volta il frame corrente e procedere al secondo blocco di disegno, consistente nella sola headerView. Dopo l'istruzione di unlockFocus, il SdR è tornato quello con origine in basso a sinistra. Il nuovo frame è proprio la zona sopra l'area di stampa, con l'esclusione dei margini sinistro e destro. Eseguo quindi uno spostamento del SdR, in modo che la successiva istruzione di drawRect: della headerView avvenga correttamente.

    // secondo blocco, cambio il frame per disegnare l'intestazione
    [ self setFrame: NSMakeRect( lm, borderSize.height * invSF - tm,
                borderSize.width * invSF - 2*lm, tm ) ] ;
    [ self lockFocus ] ;
    // sposto l'origine per disegnare lo header (un po' piu' su)
    [ tf translateXBy: 0 yBy: tm - headHeight ] ;
    [ tf concat ] ;
    // disegno lo header
    [ [ v2p headerView ] drawRect: [ self bounds] ];
    [ self unlockFocus ] ;

    // ripristino il frame iniziale
    [ self setFrame: prevFrame ] ;
    return ;
}

Conclusioni preliminari

È stato un lavoraccio, e non sono soddisfatto del risultato. Ci sono alcuni punti ancora oscuri, ed alcune magie di origine misteriosa. Tuttavia, la stampa adesso funziona, con risultati tutto sommato accettabili. Nel mio pragmatismo (quando va che tanto basta, non toccare che si guasta), non vorrei metterci più mano. Ci sono sicuramente alcuni punti che non ho chiari, altri che non ho capito per nulla, e il tutto mi da la sensazione di essere un lavoro complicato svolto solo perché non ho trovato la soluzione semplice a portata di mano. Se invece a voi appare evidente, fatemi una cortesia: ditemela, ma abbiate tatto.

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).