1/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2/*
3 * This file is part of the LibreOffice project.
4 *
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 *
9 * This file incorporates work covered by the following license notice:
10 *
11 *   Licensed to the Apache Software Foundation (ASF) under one or more
12 *   contributor license agreements. See the NOTICE file distributed
13 *   with this work for additional information regarding copyright
14 *   ownership. The ASF licenses this file to you under the Apache
15 *   License, Version 2.0 (the "License"); you may not use this file
16 *   except in compliance with the License. You may obtain a copy of
17 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18 */
19
20#include <sal/config.h>
21#include <sal/log.hxx>
22
23#include <i18nlangtag/languagetag.hxx>
24#include <rtl/ustrbuf.hxx>
25#include <vcl/print.hxx>
26#include <vcl/image.hxx>
27#include <vcl/virdev.hxx>
28#include <vcl/svapp.hxx>
29#include <vcl/unohelp.hxx>
30#include <vcl/settings.hxx>
31
32#include <osx/printview.h>
33#include <osx/salinst.h>
34#include <quartz/utils.h>
35
36#include <svdata.hxx>
37#include <strings.hrc>
38#include <printaccessoryview.hrc>
39
40#include <com/sun/star/i18n/XBreakIterator.hpp>
41#include <com/sun/star/i18n/WordType.hpp>
42
43#include <map>
44#include <string_view>
45#include <utility>
46
47using namespace vcl;
48using namespace com::sun::star;
49using namespace com::sun::star::beans;
50using namespace com::sun::star::uno;
51
52namespace {
53
54class ControllerProperties;
55
56}
57
58@interface ControlTarget : NSObject
59{
60    ControllerProperties* mpController;
61}
62-(id)initWithControllerMap: (ControllerProperties*)pController;
63-(void)triggered:(id)pSender;
64-(void)triggeredNumeric:(id)pSender;
65-(void)dealloc;
66@end
67
68@interface AquaPrintPanelAccessoryController : NSViewController< NSPrintPanelAccessorizing >
69{
70    NSPrintOperation *mpPrintOperation;
71    vcl::PrinterController *mpPrinterController;
72    PrintAccessoryViewState *mpViewState;
73}
74
75-(void)forPrintOperation:(NSPrintOperation*)pPrintOp;
76-(void)withPrinterController:(vcl::PrinterController*)pController;
77-(void)withViewState:(PrintAccessoryViewState*)pState;
78
79-(NSPrintOperation*)printOperation;
80-(vcl::PrinterController*)printerController;
81-(PrintAccessoryViewState*)viewState;
82
83-(NSSet*)keyPathsForValuesAffectingPreview;
84-(NSArray*)localizedSummaryItems;
85
86-(sal_Int32)updatePrintOperation:(sal_Int32)pLastPageCount;
87
88@end
89
90@implementation AquaPrintPanelAccessoryController
91
92-(void)forPrintOperation:(NSPrintOperation*)pPrintOp
93    { mpPrintOperation = pPrintOp; }
94
95-(void)withPrinterController:(vcl::PrinterController*)pController
96    { mpPrinterController = pController; }
97
98-(void)withViewState:(PrintAccessoryViewState*)pState
99    { mpViewState = pState; }
100
101-(NSPrintOperation*)printOperation
102    { return mpPrintOperation; }
103
104-(vcl::PrinterController*)printerController
105    { return mpPrinterController; }
106
107-(PrintAccessoryViewState*)viewState
108    { return mpViewState; }
109
110-(NSSet*)keyPathsForValuesAffectingPreview
111{
112    return [ NSSet setWithObject:@"updatePrintOperation" ];
113}
114
115-(NSArray*)localizedSummaryItems
116{
117    return [ NSArray arrayWithObject:
118               [ NSDictionary dictionary ] ];
119}
120
121-(sal_Int32)updatePrintOperation:(sal_Int32)pLastPageCount
122{
123    // page range may be changed by option choice
124    sal_Int32 nPages = mpPrinterController->getFilteredPageCount();
125
126    mpViewState->bNeedRestart = false;
127    if( nPages != pLastPageCount )
128    {
129        #if OSL_DEBUG_LEVEL > 1
130        SAL_INFO( "vcl.osx.print", "number of pages changed" <<
131                  " from " << pLastPageCount << " to " << nPages );
132        #endif
133        mpViewState->bNeedRestart = true;
134    }
135
136    NSTabView* pTabView = [[[self view] subviews] objectAtIndex:0];
137    NSTabViewItem* pItem = [pTabView selectedTabViewItem];
138    if( pItem )
139        mpViewState->nLastPage = [pTabView indexOfTabViewItem: pItem];
140    else
141        mpViewState->nLastPage = 0;
142
143    if( mpViewState->bNeedRestart )
144    {
145        // AppKit does not give a chance of changing the page count
146        // and don't let cancel the dialog either
147        // hack: send a cancel message to the modal window displaying views
148        NSWindow* pNSWindow = [NSApp modalWindow];
149        if( pNSWindow )
150            [pNSWindow cancelOperation: nil];
151        [[mpPrintOperation printInfo] setJobDisposition: NSPrintCancelJob];
152    }
153
154    return nPages;
155}
156
157@end
158
159namespace {
160
161class ControllerProperties
162{
163    std::map< int, OUString >      maTagToPropertyName;
164    std::map< int, sal_Int32 >          maTagToValueInt;
165    std::map< NSView*, NSView* >        maViewPairMap;
166    std::vector< NSObject* >            maViews;
167    int                                 mnNextTag;
168    sal_Int32                           mnLastPageCount;
169    AquaPrintPanelAccessoryController*  mpAccessoryController;
170
171public:
172    ControllerProperties( AquaPrintPanelAccessoryController* i_pAccessoryController )
173    : mnNextTag( 0 )
174    , mnLastPageCount( [i_pAccessoryController printerController]->getFilteredPageCount() )
175    , mpAccessoryController( i_pAccessoryController )
176    {
177        static_assert( SAL_N_ELEMENTS(SV_PRINT_NATIVE_STRINGS) == 5, "resources not found" );
178    }
179
180    static OUString getMoreString()
181    {
182        return VclResId(SV_PRINT_NATIVE_STRINGS[3]);
183    }
184
185    static OUString getPrintSelectionString()
186    {
187        return VclResId(SV_PRINT_NATIVE_STRINGS[4]);
188    }
189
190    int addNameTag( const OUString& i_rPropertyName )
191    {
192        int nNewTag = mnNextTag++;
193        maTagToPropertyName[ nNewTag ] = i_rPropertyName;
194        return nNewTag;
195    }
196
197    int addNameAndValueTag( const OUString& i_rPropertyName, sal_Int32 i_nValue )
198    {
199        int nNewTag = mnNextTag++;
200        maTagToPropertyName[ nNewTag ] = i_rPropertyName;
201        maTagToValueInt[ nNewTag ] = i_nValue;
202        return nNewTag;
203    }
204
205    void addObservedControl( NSObject* i_pView )
206    {
207        maViews.push_back( i_pView );
208    }
209
210    void addViewPair( NSView* i_pLeft, NSView* i_pRight )
211    {
212        maViewPairMap[ i_pLeft ] = i_pRight;
213        maViewPairMap[ i_pRight ] = i_pLeft;
214    }
215
216    NSView* getPair( NSView* i_pLeft ) const
217    {
218        NSView* pRight = nil;
219        std::map< NSView*, NSView* >::const_iterator it = maViewPairMap.find( i_pLeft );
220        if( it != maViewPairMap.end() )
221            pRight = it->second;
222        return pRight;
223    }
224
225    void changePropertyWithIntValue( int i_nTag )
226    {
227        std::map< int, OUString >::const_iterator name_it = maTagToPropertyName.find( i_nTag );
228        std::map< int, sal_Int32 >::const_iterator value_it = maTagToValueInt.find( i_nTag );
229        if( name_it != maTagToPropertyName.end() && value_it != maTagToValueInt.end() )
230        {
231            vcl::PrinterController * mpController = [mpAccessoryController printerController];
232            PropertyValue* pVal = mpController->getValue( name_it->second );
233            if( pVal )
234            {
235                pVal->Value <<= value_it->second;
236                mnLastPageCount = [mpAccessoryController updatePrintOperation: mnLastPageCount];
237            }
238        }
239    }
240
241    void changePropertyWithIntValue( int i_nTag, sal_Int64 i_nValue )
242    {
243        std::map< int, OUString >::const_iterator name_it = maTagToPropertyName.find( i_nTag );
244        if( name_it != maTagToPropertyName.end() )
245        {
246            vcl::PrinterController * mpController = [mpAccessoryController printerController];
247            PropertyValue* pVal = mpController->getValue( name_it->second );
248            if( pVal )
249            {
250                pVal->Value <<= i_nValue;
251                mnLastPageCount = [mpAccessoryController updatePrintOperation: mnLastPageCount];
252            }
253        }
254    }
255
256    void changePropertyWithBoolValue( int i_nTag, bool i_bValue )
257    {
258        std::map< int, OUString >::const_iterator name_it = maTagToPropertyName.find( i_nTag );
259        if( name_it != maTagToPropertyName.end() )
260        {
261            vcl::PrinterController * mpController = [mpAccessoryController printerController];
262            PropertyValue* pVal = mpController->getValue( name_it->second );
263            if( pVal )
264            {
265                // ugly
266                if( name_it->second == "PrintContent" )
267                   pVal->Value <<= i_bValue ? sal_Int32(2) : sal_Int32(0);
268                else
269                   pVal->Value <<= i_bValue;
270
271                mnLastPageCount = [mpAccessoryController updatePrintOperation: mnLastPageCount];
272            }
273        }
274    }
275
276    void changePropertyWithStringValue( int i_nTag, const OUString& i_rValue )
277    {
278        std::map< int, OUString >::const_iterator name_it = maTagToPropertyName.find( i_nTag );
279        if( name_it != maTagToPropertyName.end() )
280        {
281            vcl::PrinterController * mpController = [mpAccessoryController printerController];
282            PropertyValue* pVal = mpController->getValue( name_it->second );
283            if( pVal )
284            {
285                pVal->Value <<= i_rValue;
286                mnLastPageCount = [mpAccessoryController updatePrintOperation: mnLastPageCount];
287            }
288        }
289    }
290
291    void updateEnableState()
292    {
293        for( std::vector< NSObject* >::iterator it = maViews.begin(); it != maViews.end(); ++it )
294        {
295            NSObject* pObj = *it;
296            NSControl* pCtrl = nil;
297            NSCell* pCell = nil;
298            if( [pObj isKindOfClass: [NSControl class]] )
299                pCtrl = static_cast<NSControl*>(pObj);
300            else if( [pObj isKindOfClass: [NSCell class]] )
301                pCell = static_cast<NSCell*>(pObj);
302
303            int nTag = pCtrl ? [pCtrl tag] :
304                       pCell ? [pCell tag] :
305                       -1;
306
307            std::map< int, OUString >::const_iterator name_it = maTagToPropertyName.find( nTag );
308            if( name_it != maTagToPropertyName.end() && name_it->second != "PrintContent" )
309            {
310                vcl::PrinterController * mpController = [mpAccessoryController printerController];
311                bool bEnabled = mpController->isUIOptionEnabled( name_it->second ) ? YES : NO;
312                if( pCtrl )
313                {
314                    [pCtrl setEnabled: bEnabled];
315                    NSView* pOther = getPair( pCtrl );
316                    if( pOther && [pOther isKindOfClass: [NSControl class]] )
317                        [static_cast<NSControl*>(pOther) setEnabled: bEnabled];
318                }
319                else if( pCell )
320                    [pCell setEnabled: bEnabled];
321            }
322        }
323    }
324
325};
326
327}
328
329static OUString filterAccelerator( OUString const & rText )
330{
331    OUStringBuffer aBuf( rText.getLength() );
332    for( sal_Int32 nIndex = 0; nIndex != -1; )
333        aBuf.append( rText.getToken( 0, '~', nIndex ) );
334    return aBuf.makeStringAndClear();
335}
336
337@implementation ControlTarget
338
339-(id)initWithControllerMap: (ControllerProperties*)pController
340{
341    if( (self = [super init]) )
342    {
343        mpController = pController;
344    }
345    return self;
346}
347
348-(void)triggered:(id)pSender
349{
350    if( [pSender isMemberOfClass: [NSPopUpButton class]] )
351    {
352        NSPopUpButton* pBtn = static_cast<NSPopUpButton*>(pSender);
353        NSMenuItem* pSelected = [pBtn selectedItem];
354        if( pSelected )
355        {
356            int nTag = [pSelected tag];
357            mpController->changePropertyWithIntValue( nTag );
358        }
359    }
360    else if( [pSender isMemberOfClass: [NSButton class]] )
361    {
362        NSButton* pBtn = static_cast<NSButton*>(pSender);
363        int nTag = [pBtn tag];
364        mpController->changePropertyWithBoolValue( nTag, [pBtn state] == NSControlStateValueOn );
365    }
366    else if( [pSender isMemberOfClass: [NSMatrix class]] )
367    {
368        NSObject* pObj = [static_cast<NSMatrix*>(pSender) selectedCell];
369        if( [pObj isMemberOfClass: [NSButtonCell class]] )
370        {
371            NSButtonCell* pCell = static_cast<NSButtonCell*>(pObj);
372            int nTag = [pCell tag];
373            mpController->changePropertyWithIntValue( nTag );
374        }
375    }
376    else if( [pSender isMemberOfClass: [NSTextField class]] )
377    {
378        NSTextField* pField = static_cast<NSTextField*>(pSender);
379        int nTag = [pField tag];
380        OUString aValue = GetOUString( [pSender stringValue] );
381        mpController->changePropertyWithStringValue( nTag, aValue );
382    }
383    else
384    {
385        SAL_INFO( "vcl.osx.print", "Unsupported class" <<
386                  ( [pSender class] ? [NSStringFromClass([pSender class]) UTF8String] : "nil" ) );
387    }
388    mpController->updateEnableState();
389}
390
391-(void)triggeredNumeric:(id)pSender
392{
393    if( [pSender isMemberOfClass: [NSTextField class]] )
394    {
395        NSTextField* pField = static_cast<NSTextField*>(pSender);
396        int nTag = [pField tag];
397        sal_Int64 nValue = [pField intValue];
398
399        NSView* pOther = mpController->getPair( pField );
400        if( pOther )
401            [static_cast<NSControl*>(pOther) setIntValue: nValue];
402
403        mpController->changePropertyWithIntValue( nTag, nValue );
404    }
405    else if( [pSender isMemberOfClass: [NSStepper class]] )
406    {
407        NSStepper* pStep = static_cast<NSStepper*>(pSender);
408        int nTag = [pStep tag];
409        sal_Int64 nValue = [pStep intValue];
410
411        NSView* pOther = mpController->getPair( pStep );
412        if( pOther )
413            [static_cast<NSControl*>(pOther) setIntValue: nValue];
414
415        mpController->changePropertyWithIntValue( nTag, nValue );
416    }
417    else
418    {
419        SAL_INFO( "vcl.osx.print", "Unsupported class" <<
420                  ([pSender class] ? [NSStringFromClass([pSender class]) UTF8String] : "nil") );
421    }
422    mpController->updateEnableState();
423}
424
425-(void)dealloc
426{
427    delete mpController;
428    [super dealloc];
429}
430
431@end
432
433namespace {
434
435struct ColumnItem
436{
437    NSControl*      pControl;
438    CGFloat         nOffset;
439    NSControl*      pSubControl;
440
441    ColumnItem( NSControl* i_pControl = nil, CGFloat i_nOffset = 0, NSControl* i_pSub = nil )
442    : pControl( i_pControl )
443    , nOffset( i_nOffset )
444    , pSubControl( i_pSub )
445    {}
446
447    CGFloat getWidth() const
448    {
449        CGFloat nWidth = 0;
450        if( pControl )
451        {
452            NSRect aCtrlRect = [pControl frame];
453            nWidth = aCtrlRect.size.width;
454            nWidth += nOffset;
455            if( pSubControl )
456            {
457                NSRect aSubRect = [pSubControl frame];
458                nWidth += aSubRect.size.width;
459                nWidth += aSubRect.origin.x - (aCtrlRect.origin.x + aCtrlRect.size.width);
460            }
461        }
462        return nWidth;
463    }
464};
465
466}
467
468static void adjustViewAndChildren( NSView* pNSView, NSSize& rMaxSize,
469                                   std::vector< ColumnItem >& rLeftColumn,
470                                   std::vector< ColumnItem >& rRightColumn
471                                  )
472{
473    // balance columns
474
475    // first get overall column widths
476    CGFloat nLeftWidth = 0;
477    CGFloat nRightWidth = 0;
478    for( size_t i = 0; i < rLeftColumn.size(); i++ )
479    {
480        CGFloat nW = rLeftColumn[i].getWidth();
481        if( nW > nLeftWidth )
482            nLeftWidth = nW;
483    }
484    for( size_t i = 0; i < rRightColumn.size(); i++ )
485    {
486        CGFloat nW = rRightColumn[i].getWidth();
487        if( nW > nRightWidth )
488            nRightWidth = nW;
489    }
490
491    // right align left column
492    for( size_t i = 0; i < rLeftColumn.size(); i++ )
493    {
494        if( rLeftColumn[i].pControl )
495        {
496            NSRect aCtrlRect = [rLeftColumn[i].pControl frame];
497            CGFloat nX = nLeftWidth - aCtrlRect.size.width;
498            if( rLeftColumn[i].pSubControl )
499            {
500                NSRect aSubRect = [rLeftColumn[i].pSubControl frame];
501                nX -= aSubRect.size.width + (aSubRect.origin.x - (aCtrlRect.origin.x + aCtrlRect.size.width));
502                aSubRect.origin.x = nLeftWidth - aSubRect.size.width;
503                [rLeftColumn[i].pSubControl setFrame: aSubRect];
504            }
505            aCtrlRect.origin.x = nX;
506            [rLeftColumn[i].pControl setFrame: aCtrlRect];
507        }
508    }
509
510    // left align right column
511    for( size_t i = 0; i < rRightColumn.size(); i++ )
512    {
513        if( rRightColumn[i].pControl )
514        {
515            NSRect aCtrlRect = [rRightColumn[i].pControl frame];
516            CGFloat nX = nLeftWidth + 3;
517            if( rRightColumn[i].pSubControl )
518            {
519                NSRect aSubRect = [rRightColumn[i].pSubControl frame];
520                aSubRect.origin.x = nX + aSubRect.origin.x - aCtrlRect.origin.x;
521                [rRightColumn[i].pSubControl setFrame: aSubRect];
522            }
523            aCtrlRect.origin.x = nX;
524            [rRightColumn[i].pControl setFrame: aCtrlRect];
525        }
526    }
527
528    NSArray* pSubViews = [pNSView subviews];
529    unsigned int nViews = [pSubViews count];
530    NSRect aUnion = NSZeroRect;
531
532    // get the combined frame of all subviews
533    for( unsigned int n = 0; n < nViews; n++ )
534    {
535        aUnion = NSUnionRect( aUnion, [[pSubViews objectAtIndex: n] frame] );
536    }
537
538    // move everything so it will fit
539    for( unsigned int n = 0; n < nViews; n++ )
540    {
541        NSView* pCurSubView = [pSubViews objectAtIndex: n];
542        NSRect aFrame = [pCurSubView frame];
543        aFrame.origin.x -= aUnion.origin.x - 5;
544        aFrame.origin.y -= aUnion.origin.y - 5;
545        [pCurSubView setFrame: aFrame];
546    }
547
548    // resize the view itself
549    aUnion.size.height += 10;
550    aUnion.size.width += 20;
551    [pNSView setFrameSize: aUnion.size];
552
553    if( aUnion.size.width > rMaxSize.width )
554        rMaxSize.width = aUnion.size.width;
555    if( aUnion.size.height > rMaxSize.height )
556        rMaxSize.height = aUnion.size.height;
557}
558
559static void adjustTabViews( NSTabView* pTabView, NSSize aTabSize )
560{
561    // loop over all contained tab pages
562    NSArray* pTabbedViews = [pTabView tabViewItems];
563    int nViews = [pTabbedViews count];
564    for( int i = 0; i < nViews; i++ )
565    {
566        NSTabViewItem* pItem = static_cast<NSTabViewItem*>([pTabbedViews objectAtIndex: i]);
567        NSView* pNSView = [pItem view];
568        if( pNSView )
569        {
570            NSRect aRect = [pNSView frame];
571            double nDiff = aTabSize.height - aRect.size.height;
572            aRect.size = aTabSize;
573            [pNSView setFrame: aRect];
574
575            NSArray* pSubViews = [pNSView subviews];
576            unsigned int nSubViews = [pSubViews count];
577
578            // move everything up
579            for( unsigned int n = 0; n < nSubViews; n++ )
580            {
581                NSView* pCurSubView = [pSubViews objectAtIndex: n];
582                NSRect aFrame = [pCurSubView frame];
583                aFrame.origin.y += nDiff;
584                // give separators the correct width
585                // separators are currently the only NSBoxes we use
586                if( [pCurSubView isMemberOfClass: [NSBox class]] )
587                {
588                    aFrame.size.width = aTabSize.width - aFrame.origin.x - 10;
589                }
590                [pCurSubView setFrame: aFrame];
591            }
592        }
593    }
594}
595
596static NSControl* createLabel( const OUString& i_rText )
597{
598    NSString* pText = CreateNSString( i_rText );
599    NSRect aTextRect = { NSZeroPoint, {20, 15} };
600    NSTextField* pTextView = [[NSTextField alloc] initWithFrame: aTextRect];
601    [pTextView setFont: [NSFont controlContentFontOfSize: 0]];
602    [pTextView setEditable: NO];
603    [pTextView setSelectable: NO];
604    [pTextView setDrawsBackground: NO];
605    [pTextView setBordered: NO];
606    [pTextView setStringValue: pText];
607    [pTextView sizeToFit];
608    [pText release];
609    return pTextView;
610}
611
612static sal_Int32 findBreak( const OUString& i_rText, sal_Int32 i_nPos )
613{
614    sal_Int32 nRet = i_rText.getLength();
615    Reference< i18n::XBreakIterator > xBI( vcl::unohelper::CreateBreakIterator() );
616    if( xBI.is() )
617    {
618        i18n::Boundary aBoundary =
619                xBI->getWordBoundary( i_rText, i_nPos,
620                                      Application::GetSettings().GetLanguageTag().getLocale(),
621                                      i18n::WordType::ANYWORD_IGNOREWHITESPACES,
622                                      true );
623        nRet = aBoundary.endPos;
624    }
625    return nRet;
626}
627
628static void linebreakCell( NSCell* pBtn, const OUString& i_rText )
629{
630    NSString* pText = CreateNSString( i_rText );
631    [pBtn setTitle: pText];
632    [pText release];
633    NSSize aSize = [pBtn cellSize];
634    if( aSize.width > 280 )
635    {
636        // need two lines
637        sal_Int32 nLen = i_rText.getLength();
638        sal_Int32 nIndex = nLen / 2;
639        nIndex = findBreak( i_rText, nIndex );
640        if( nIndex < nLen )
641        {
642            OUStringBuffer aBuf( i_rText );
643            aBuf[nIndex] = '\n';
644            pText = CreateNSString( aBuf.makeStringAndClear() );
645            [pBtn setTitle: pText];
646            [pText release];
647        }
648    }
649}
650
651static void addSubgroup( NSView* pCurParent, CGFloat& rCurY, const OUString& rText )
652{
653    NSControl* pTextView = createLabel( rText );
654    [pCurParent addSubview: [pTextView autorelease]];
655    NSRect aTextRect = [pTextView frame];
656    // move to nCurY
657    aTextRect.origin.y = rCurY - aTextRect.size.height;
658    [pTextView setFrame: aTextRect];
659
660    NSRect aSepRect = { { aTextRect.size.width + 1, aTextRect.origin.y }, { 100, 6 } };
661    NSBox* pBox = [[NSBox alloc] initWithFrame: aSepRect];
662    [pBox setBoxType: NSBoxSeparator];
663    [pCurParent addSubview: [pBox autorelease]];
664
665    // update nCurY
666    rCurY = aTextRect.origin.y - 5;
667}
668
669static void addBool( NSView* pCurParent, CGFloat rCurX, CGFloat& rCurY, CGFloat nAttachOffset,
670                    const OUString& rText, bool bEnabled,
671                    const OUString& rProperty, bool bValue,
672                    std::vector<ColumnItem >& rRightColumn,
673                    ControllerProperties* pControllerProperties,
674                    ControlTarget* pCtrlTarget
675                    )
676{
677    NSRect aCheckRect = { { rCurX + nAttachOffset, 0 }, { 0, 15 } };
678    NSButton* pBtn = [[NSButton alloc] initWithFrame: aCheckRect];
679    [pBtn setButtonType: NSButtonTypeSwitch];
680    [pBtn setState: bValue ? NSControlStateValueOn : NSControlStateValueOff];
681    if( ! bEnabled )
682        [pBtn setEnabled: NO];
683    linebreakCell( [pBtn cell], rText );
684    [pBtn sizeToFit];
685
686    rRightColumn.push_back( ColumnItem( pBtn ) );
687
688    // connect target
689    [pBtn setTarget: pCtrlTarget];
690    [pBtn setAction: @selector(triggered:)];
691    int nTag = pControllerProperties->addNameTag( rProperty );
692    pControllerProperties->addObservedControl( pBtn );
693    [pBtn setTag: nTag];
694
695    aCheckRect = [pBtn frame];
696    // #i115837# add a murphy factor; it can apparently occasionally happen
697    // that sizeToFit does not a perfect job and that the button linebreaks again
698    // if - and only if - there is already a '\n' contained in the text and the width
699    // is minimally of
700    aCheckRect.size.width += 1;
701
702    // move to rCurY
703    aCheckRect.origin.y = rCurY - aCheckRect.size.height;
704    [pBtn setFrame: aCheckRect];
705
706    [pCurParent addSubview: [pBtn autorelease]];
707
708    // update rCurY
709    rCurY = aCheckRect.origin.y - 5;
710}
711
712static void addRadio( NSView* pCurParent, CGFloat rCurX, CGFloat& rCurY, CGFloat nAttachOffset,
713                     const OUString& rText,
714                     const OUString& rProperty, Sequence<OUString> const & rChoices, sal_Int32 nSelectValue,
715                     std::vector<ColumnItem >& rLeftColumn,
716                     std::vector<ColumnItem >& rRightColumn,
717                     ControllerProperties* pControllerProperties,
718                     ControlTarget* pCtrlTarget
719                     )
720{
721    CGFloat nOff = 0;
722    if( rText.getLength() )
723    {
724        // add a label
725        NSControl* pTextView = createLabel( rText );
726        NSRect aTextRect = [pTextView frame];
727        aTextRect.origin.x = rCurX + nAttachOffset;
728        [pCurParent addSubview: [pTextView autorelease]];
729
730        rLeftColumn.push_back( ColumnItem( pTextView ) );
731
732        // move to nCurY
733        aTextRect.origin.y = rCurY - aTextRect.size.height;
734        [pTextView setFrame: aTextRect];
735
736        // update nCurY
737        rCurY = aTextRect.origin.y - 5;
738
739        // indent the radio group relative to the text
740        // nOff = 20;
741    }
742
743    // setup radio matrix
744    NSButtonCell* pProto = [[NSButtonCell alloc] init];
745
746    NSRect aRadioRect = { { rCurX + nOff, 0 },
747                          { 280 - rCurX,
748                            static_cast<CGFloat>(5*rChoices.getLength()) } };
749    [pProto setTitle: @"RadioButtonGroup"];
750    [pProto setButtonType: NSButtonTypeRadio];
751    NSMatrix* pMatrix = [[NSMatrix alloc] initWithFrame: aRadioRect
752                                          mode: NSRadioModeMatrix
753                                          prototype: static_cast<NSCell*>(pProto)
754                                          numberOfRows: rChoices.getLength()
755                                          numberOfColumns: 1];
756    // set individual titles
757    NSArray* pCells = [pMatrix cells];
758    for( sal_Int32 m = 0; m < rChoices.getLength(); m++ )
759    {
760        NSCell* pCell = [pCells objectAtIndex: m];
761        linebreakCell( pCell, filterAccelerator( rChoices[m] ) );
762        // connect target and action
763        [pCell setTarget: pCtrlTarget];
764        [pCell setAction: @selector(triggered:)];
765        int nTag = pControllerProperties->addNameAndValueTag( rProperty, m );
766        pControllerProperties->addObservedControl( pCell );
767        [pCell setTag: nTag];
768        // set current selection
769        if( nSelectValue == m )
770            [pMatrix selectCellAtRow: m column: 0];
771    }
772    [pMatrix sizeToFit];
773    aRadioRect = [pMatrix frame];
774
775    // move it down, so it comes to the correct position
776    aRadioRect.origin.y = rCurY - aRadioRect.size.height;
777    [pMatrix setFrame: aRadioRect];
778    [pCurParent addSubview: [pMatrix autorelease]];
779
780    rRightColumn.push_back( ColumnItem( pMatrix ) );
781
782    // update nCurY
783    rCurY = aRadioRect.origin.y - 5;
784
785    [pProto release];
786}
787
788static void addList( NSView* pCurParent, CGFloat& rCurX, CGFloat& rCurY, CGFloat /*nAttachOffset*/,
789                    const OUString& rText,
790                    const OUString& rProperty, Sequence<OUString> const & rChoices, sal_Int32 nSelectValue,
791                    std::vector<ColumnItem >& rLeftColumn,
792                    std::vector<ColumnItem >& rRightColumn,
793                    ControllerProperties* pControllerProperties,
794                    ControlTarget* pCtrlTarget
795                    )
796{
797    // don't indent attached lists, looks bad in the existing cases
798    NSControl* pTextView = createLabel( rText );
799    [pCurParent addSubview: [pTextView autorelease]];
800    rLeftColumn.push_back( ColumnItem( pTextView ) );
801    NSRect aTextRect = [pTextView frame];
802    aTextRect.origin.x = rCurX /* + nAttachOffset*/;
803
804    // don't indent attached lists, looks bad in the existing cases
805    NSRect aBtnRect = { { rCurX /*+ nAttachOffset*/ + aTextRect.size.width, 0 }, { 0, 15 } };
806    NSPopUpButton* pBtn = [[NSPopUpButton alloc] initWithFrame: aBtnRect pullsDown: NO];
807
808    // iterate options
809    for( sal_Int32 m = 0; m < rChoices.getLength(); m++ )
810    {
811        NSString* pItemText = CreateNSString( rChoices[m] );
812        [pBtn addItemWithTitle: pItemText];
813        NSMenuItem* pItem = [pBtn itemWithTitle: pItemText];
814        int nTag = pControllerProperties->addNameAndValueTag( rProperty, m );
815        [pItem setTag: nTag];
816        [pItemText release];
817    }
818
819    [pBtn selectItemAtIndex: nSelectValue];
820
821    // add the button to observed controls for enabled state changes
822    // also add a tag just for this purpose
823    pControllerProperties->addObservedControl( pBtn );
824    [pBtn setTag: pControllerProperties->addNameTag( rProperty )];
825
826    [pBtn sizeToFit];
827    [pCurParent addSubview: [pBtn autorelease]];
828
829    rRightColumn.push_back( ColumnItem( pBtn ) );
830
831    // connect target and action
832    [pBtn setTarget: pCtrlTarget];
833    [pBtn setAction: @selector(triggered:)];
834
835    // move to nCurY
836    aBtnRect = [pBtn frame];
837    aBtnRect.origin.y = rCurY - aBtnRect.size.height;
838    [pBtn setFrame: aBtnRect];
839
840    // align label
841    aTextRect.origin.y = aBtnRect.origin.y + (aBtnRect.size.height - aTextRect.size.height)/2;
842    [pTextView setFrame: aTextRect];
843
844    // update rCurY
845    rCurY = aBtnRect.origin.y - 5;
846}
847
848static void addEdit( NSView* pCurParent, CGFloat rCurX, CGFloat& rCurY, CGFloat nAttachOffset,
849                    std::u16string_view rCtrlType,
850                    const OUString& rText,
851                    const OUString& rProperty, const PropertyValue* pValue,
852                    sal_Int64 nMinValue, sal_Int64 nMaxValue,
853                    std::vector<ColumnItem >& rLeftColumn,
854                    std::vector<ColumnItem >& rRightColumn,
855                    ControllerProperties* pControllerProperties,
856                    ControlTarget* pCtrlTarget
857                    )
858{
859    CGFloat nOff = 0;
860    if( rText.getLength() )
861    {
862        // add a label
863        NSControl* pTextView = createLabel( rText );
864        [pCurParent addSubview: [pTextView autorelease]];
865
866        rLeftColumn.push_back( ColumnItem( pTextView ) );
867
868        // move to nCurY
869        NSRect aTextRect = [pTextView frame];
870        aTextRect.origin.x = rCurX + nAttachOffset;
871        aTextRect.origin.y = rCurY - aTextRect.size.height;
872        [pTextView setFrame: aTextRect];
873
874        // update nCurY
875        rCurY = aTextRect.origin.y - 5;
876
877        // and set the offset for the real edit field
878        nOff = aTextRect.size.width + 5;
879    }
880
881    NSRect aFieldRect = { { rCurX + nOff + nAttachOffset, 0 }, { 100, 25 } };
882    NSTextField* pFieldView = [[NSTextField alloc] initWithFrame: aFieldRect];
883    [pFieldView setEditable: YES];
884    [pFieldView setSelectable: YES];
885    [pFieldView setDrawsBackground: YES];
886    [pFieldView sizeToFit]; // FIXME: this does nothing
887    [pCurParent addSubview: [pFieldView autorelease]];
888
889    rRightColumn.push_back( ColumnItem( pFieldView ) );
890
891    // add the field to observed controls for enabled state changes
892    // also add a tag just for this purpose
893    pControllerProperties->addObservedControl( pFieldView );
894    int nTag = pControllerProperties->addNameTag( rProperty );
895    [pFieldView setTag: nTag];
896    // pControllerProperties->addNamedView( pFieldView, aPropertyName );
897
898    // move to nCurY
899    aFieldRect.origin.y = rCurY - aFieldRect.size.height;
900    [pFieldView setFrame: aFieldRect];
901
902    if( rCtrlType == u"Range" )
903    {
904        // add a stepper control
905        NSRect aStepFrame = { { aFieldRect.origin.x + aFieldRect.size.width + 5,
906                                aFieldRect.origin.y },
907                            { 15, aFieldRect.size.height } };
908        NSStepper* pStep = [[NSStepper alloc] initWithFrame: aStepFrame];
909        [pStep setIncrement: 1];
910        [pStep setValueWraps: NO];
911        [pStep setTag: nTag];
912        [pCurParent addSubview: [pStep autorelease]];
913
914        rRightColumn.back().pSubControl = pStep;
915
916        pControllerProperties->addObservedControl( pStep );
917        [pStep setTarget: pCtrlTarget];
918        [pStep setAction: @selector(triggered:)];
919
920        // constrain the text field to decimal numbers
921        NSNumberFormatter* pFormatter = [[NSNumberFormatter alloc] init];
922        [pFormatter setFormatterBehavior: NSNumberFormatterBehavior10_4];
923        [pFormatter setNumberStyle: NSNumberFormatterDecimalStyle];
924        [pFormatter setAllowsFloats: NO];
925        [pFormatter setMaximumFractionDigits: 0];
926        if( nMinValue != nMaxValue )
927        {
928            [pFormatter setMinimum: [[NSNumber numberWithInt: nMinValue] autorelease]];
929            [pStep setMinValue: nMinValue];
930            [pFormatter setMaximum: [[NSNumber numberWithInt: nMaxValue] autorelease]];
931            [pStep setMaxValue: nMaxValue];
932        }
933        [pFieldView setFormatter: pFormatter];
934
935        sal_Int64 nSelectVal = 0;
936        if( pValue && pValue->Value.hasValue() )
937            pValue->Value >>= nSelectVal;
938
939        [pFieldView setIntValue: nSelectVal];
940        [pStep setIntValue: nSelectVal];
941
942        pControllerProperties->addViewPair( pFieldView, pStep );
943        // connect target and action
944        [pFieldView setTarget: pCtrlTarget];
945        [pFieldView setAction: @selector(triggeredNumeric:)];
946        [pStep setTarget: pCtrlTarget];
947        [pStep setAction: @selector(triggeredNumeric:)];
948    }
949    else
950    {
951        // connect target and action
952        [pFieldView setTarget: pCtrlTarget];
953        [pFieldView setAction: @selector(triggered:)];
954
955        if( pValue && pValue->Value.hasValue() )
956        {
957            OUString aValue;
958            pValue->Value >>= aValue;
959            if( aValue.getLength() )
960            {
961                NSString* pText = CreateNSString( aValue );
962                [pFieldView setStringValue: pText];
963                [pText release];
964            }
965        }
966    }
967
968    // update nCurY
969    rCurY = aFieldRect.origin.y - 5;
970}
971
972@implementation AquaPrintAccessoryView
973
974+(NSObject*)setupPrinterPanel: (NSPrintOperation*)pOp
975               withController: (vcl::PrinterController*)pController
976                    withState: (PrintAccessoryViewState*)pState
977{
978    const Sequence< PropertyValue >& rOptions( pController->getUIOptions() );
979    if( rOptions.getLength() == 0 )
980        return nil;
981
982    NSRect aViewFrame = { NSZeroPoint, { 600, 400 } };
983    NSRect aTabViewFrame = aViewFrame;
984
985    NSView* pAccessoryView = [[NSView alloc] initWithFrame: aViewFrame];
986    NSTabView* pTabView = [[NSTabView alloc] initWithFrame: aTabViewFrame];
987    [pAccessoryView addSubview: [pTabView autorelease]];
988
989    // create the accessory controller
990    AquaPrintPanelAccessoryController* pAccessoryController =
991            [[AquaPrintPanelAccessoryController alloc] initWithNibName: nil bundle: nil];
992    [pAccessoryController setView: [pAccessoryView autorelease]];
993    [pAccessoryController forPrintOperation: pOp];
994    [pAccessoryController withPrinterController: pController];
995    [pAccessoryController withViewState: pState];
996
997    NSView* pCurParent = nullptr;
998    CGFloat nCurY = 0;
999    CGFloat nCurX = 0;
1000    NSSize aMaxTabSize = NSZeroSize;
1001
1002    ControllerProperties* pControllerProperties = new ControllerProperties( pAccessoryController );
1003    ControlTarget* pCtrlTarget = [[ControlTarget alloc] initWithControllerMap: pControllerProperties];
1004
1005    std::vector< ColumnItem > aLeftColumn, aRightColumn;
1006
1007    // ugly:
1008    // prepend a "selection" checkbox if the properties have such a selection in PrintContent
1009    bool bAddSelectionCheckBox = false, bSelectionBoxEnabled = false, bSelectionBoxChecked = false;
1010
1011    for( const PropertyValue & prop : rOptions )
1012    {
1013        Sequence< beans::PropertyValue > aOptProp;
1014        prop.Value >>= aOptProp;
1015
1016        OUString aCtrlType;
1017        OUString aPropertyName;
1018        Sequence< OUString > aChoices;
1019        Sequence< sal_Bool > aChoicesDisabled;
1020        sal_Int32 aSelectionChecked = 0;
1021        for( const beans::PropertyValue& rEntry : std::as_const(aOptProp) )
1022        {
1023            if( rEntry.Name == "ControlType" )
1024            {
1025                rEntry.Value >>= aCtrlType;
1026            }
1027            else if( rEntry.Name == "Choices" )
1028            {
1029                rEntry.Value >>= aChoices;
1030            }
1031            else if( rEntry.Name == "ChoicesDisabled" )
1032            {
1033                rEntry.Value >>= aChoicesDisabled;
1034            }
1035            else if( rEntry.Name == "Property" )
1036            {
1037                PropertyValue aVal;
1038                rEntry.Value >>= aVal;
1039                aPropertyName = aVal.Name;
1040                if( aPropertyName == "PrintContent" )
1041                    aVal.Value >>= aSelectionChecked;
1042            }
1043        }
1044        if( aCtrlType == "Radio" &&
1045            aPropertyName == "PrintContent" &&
1046            aChoices.getLength() > 2 )
1047        {
1048            bAddSelectionCheckBox = true;
1049            bSelectionBoxEnabled = aChoicesDisabled.getLength() < 2 || ! aChoicesDisabled[2];
1050            bSelectionBoxChecked = (aSelectionChecked==2);
1051            break;
1052        }
1053    }
1054
1055    for( const PropertyValue & prop : rOptions )
1056    {
1057        Sequence< beans::PropertyValue > aOptProp;
1058        prop.Value >>= aOptProp;
1059
1060        // extract ui element
1061        OUString aCtrlType;
1062        OUString aText;
1063        OUString aPropertyName;
1064        OUString aGroupHint;
1065        Sequence< OUString > aChoices;
1066        sal_Int64 nMinValue = 0, nMaxValue = 0;
1067        CGFloat nAttachOffset = 0;
1068        bool bIgnore = false;
1069
1070        for( const beans::PropertyValue& rEntry : std::as_const(aOptProp) )
1071        {
1072            if( rEntry.Name == "Text" )
1073            {
1074                rEntry.Value >>= aText;
1075                aText = filterAccelerator( aText );
1076            }
1077            else if( rEntry.Name == "ControlType" )
1078            {
1079                rEntry.Value >>= aCtrlType;
1080            }
1081            else if( rEntry.Name == "Choices" )
1082            {
1083                rEntry.Value >>= aChoices;
1084            }
1085            else if( rEntry.Name == "Property" )
1086            {
1087                PropertyValue aVal;
1088                rEntry.Value >>= aVal;
1089                aPropertyName = aVal.Name;
1090            }
1091            else if( rEntry.Name == "MinValue" )
1092            {
1093                rEntry.Value >>= nMinValue;
1094            }
1095            else if( rEntry.Name == "MaxValue" )
1096            {
1097                rEntry.Value >>= nMaxValue;
1098            }
1099            else if( rEntry.Name == "AttachToDependency" )
1100            {
1101                nAttachOffset = 20;
1102            }
1103            else if( rEntry.Name == "InternalUIOnly" )
1104            {
1105                bool bValue = false;
1106                rEntry.Value >>= bValue;
1107                bIgnore = bValue;
1108            }
1109            else if( rEntry.Name == "GroupingHint" )
1110            {
1111                rEntry.Value >>= aGroupHint;
1112            }
1113        }
1114
1115        if( aCtrlType == "Group" ||
1116            aCtrlType == "Subgroup" ||
1117            aCtrlType == "Radio" ||
1118            aCtrlType == "List"  ||
1119            aCtrlType == "Edit"  ||
1120            aCtrlType == "Range"  ||
1121            aCtrlType == "Bool" )
1122        {
1123            bool bIgnoreSubgroup = false;
1124
1125            // with `setAccessoryView' method only one accessory view can be set
1126            // so create this single accessory view as tabbed for grouping
1127            if( aCtrlType == "Group"
1128                || ! pCurParent
1129                || ( aCtrlType == "Subgroup" && nCurY < -250 && ! bIgnore )
1130               )
1131            {
1132                OUString aGroupTitle( aText );
1133                if( aCtrlType == "Subgroup" )
1134                    aGroupTitle = ControllerProperties::getMoreString();
1135
1136                // set size of current parent
1137                if( pCurParent )
1138                    adjustViewAndChildren( pCurParent, aMaxTabSize, aLeftColumn, aRightColumn );
1139
1140                // new tab item
1141                if( ! aText.getLength() )
1142                    aText = "OOo";
1143                NSString* pLabel = CreateNSString( aGroupTitle );
1144                NSTabViewItem* pItem = [[NSTabViewItem alloc] initWithIdentifier: pLabel ];
1145                [pItem setLabel: pLabel];
1146                [pTabView addTabViewItem: pItem];
1147                pCurParent = [[NSView alloc] initWithFrame: aTabViewFrame];
1148                [pItem setView: pCurParent];
1149                [pLabel release];
1150
1151                nCurX = 20; // reset indent
1152                nCurY = 0;  // reset Y
1153                // clear columns
1154                aLeftColumn.clear();
1155                aRightColumn.clear();
1156
1157                if( bAddSelectionCheckBox )
1158                {
1159                    addBool( pCurParent, nCurX, nCurY, 0,
1160                             ControllerProperties::getPrintSelectionString(), bSelectionBoxEnabled,
1161                             "PrintContent", bSelectionBoxChecked,
1162                             aRightColumn, pControllerProperties, pCtrlTarget );
1163                    bAddSelectionCheckBox = false;
1164                }
1165            }
1166
1167            if( aCtrlType == "Subgroup" && pCurParent )
1168            {
1169                bIgnoreSubgroup = bIgnore;
1170                if( bIgnore )
1171                    continue;
1172
1173                addSubgroup( pCurParent, nCurY, aText );
1174            }
1175            else if( bIgnoreSubgroup || bIgnore )
1176            {
1177                continue;
1178            }
1179            else if( aCtrlType == "Bool" && pCurParent )
1180            {
1181                bool bVal = false;
1182                PropertyValue* pVal = pController->getValue( aPropertyName );
1183                if( pVal )
1184                    pVal->Value >>= bVal;
1185                addBool( pCurParent, nCurX, nCurY, nAttachOffset,
1186                         aText, true, aPropertyName, bVal,
1187                         aRightColumn, pControllerProperties, pCtrlTarget );
1188            }
1189            else if( aCtrlType == "Radio" && pCurParent )
1190            {
1191                // get currently selected value
1192                sal_Int32 nSelectVal = 0;
1193                PropertyValue* pVal = pController->getValue( aPropertyName );
1194                if( pVal && pVal->Value.hasValue() )
1195                    pVal->Value >>= nSelectVal;
1196
1197                addRadio( pCurParent, nCurX, nCurY, nAttachOffset,
1198                          aText, aPropertyName, aChoices, nSelectVal,
1199                          aLeftColumn, aRightColumn,
1200                          pControllerProperties, pCtrlTarget );
1201            }
1202            else if( aCtrlType == "List" && pCurParent )
1203            {
1204                PropertyValue* pVal = pController->getValue( aPropertyName );
1205                sal_Int32 aSelectVal = 0;
1206                if( pVal && pVal->Value.hasValue() )
1207                    pVal->Value >>= aSelectVal;
1208
1209                addList( pCurParent, nCurX, nCurY, nAttachOffset,
1210                         aText, aPropertyName, aChoices, aSelectVal,
1211                         aLeftColumn, aRightColumn,
1212                         pControllerProperties, pCtrlTarget );
1213            }
1214            else if( (aCtrlType == "Edit"
1215                || aCtrlType == "Range") && pCurParent )
1216            {
1217                // current value
1218                PropertyValue* pVal = pController->getValue( aPropertyName );
1219                addEdit( pCurParent, nCurX, nCurY, nAttachOffset,
1220                         aCtrlType, aText, aPropertyName, pVal,
1221                         nMinValue, nMaxValue,
1222                         aLeftColumn, aRightColumn,
1223                         pControllerProperties, pCtrlTarget );
1224            }
1225        }
1226        else
1227        {
1228            SAL_INFO( "vcl.osx.print", "Unsupported UI option \"" << aCtrlType << "\"");
1229        }
1230    }
1231
1232    pControllerProperties->updateEnableState();
1233    adjustViewAndChildren( pCurParent, aMaxTabSize, aLeftColumn, aRightColumn );
1234
1235    // now reposition everything again so it is upper bound
1236    adjustTabViews( pTabView, aMaxTabSize );
1237
1238    // find the minimum needed tab size
1239    NSSize aTabCtrlSize = [pTabView minimumSize];
1240    aTabCtrlSize.height += aMaxTabSize.height + 10;
1241    if( aTabCtrlSize.width < aMaxTabSize.width + 10 )
1242        aTabCtrlSize.width = aMaxTabSize.width + 10;
1243    [pTabView setFrameSize: aTabCtrlSize];
1244    aViewFrame.size.width = aTabCtrlSize.width + aTabViewFrame.origin.x;
1245    aViewFrame.size.height = aTabCtrlSize.height + aTabViewFrame.origin.y;
1246    [pAccessoryView setFrameSize: aViewFrame.size];
1247
1248    // get the print panel
1249    NSPrintPanel* pPrintPanel = [pOp printPanel];
1250    [pPrintPanel setOptions: [pPrintPanel options] | NSPrintPanelShowsPreview];
1251    // add the accessory controller to the panel
1252    [pPrintPanel addAccessoryController: [pAccessoryController autorelease]];
1253
1254    // set the current selected tab item
1255    if( pState->nLastPage >= 0 && pState->nLastPage < [pTabView numberOfTabViewItems] )
1256        [pTabView selectTabViewItemAtIndex: pState->nLastPage];
1257
1258    return pCtrlTarget;
1259}
1260
1261@end
1262
1263/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
1264