Browse Source

AudioUnits: eliminated the JucePlugin_AUCocoaViewClassName parameter - this is no longer needed. Also rearranged the way AUPlugInDispatch.cpp is included, to improve compatibility. (You may need to re-save your projects in the introjucer to build with these changes)

tags/2021-05-28
jules 12 years ago
parent
commit
4fe02d2c8d
5 changed files with 147 additions and 172 deletions
  1. +0
    -3
      extras/Introjucer/Source/Project/jucer_AudioPluginModule.h
  2. +0
    -6
      extras/audio plugin demo/Builds/MacOSX/JuceDemoPlugin.xcodeproj/project.pbxproj
  3. +0
    -3
      extras/audio plugin demo/JuceLibraryCode/AppConfig.h
  4. +147
    -156
      modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm
  5. +0
    -4
      modules/juce_audio_plugin_client/utility/juce_CheckSettingMacros.h

+ 0
- 3
extras/Introjucer/Source/Project/jucer_AudioPluginModule.h View File

@@ -149,7 +149,6 @@ namespace
flags.set ("JucePlugin_AUExportPrefixQuoted", getPluginAUExportPrefix (project).toString().quoted());
flags.set ("JucePlugin_AUManufacturerCode", "JucePlugin_ManufacturerCode");
flags.set ("JucePlugin_CFBundleIdentifier", project.getBundleIdentifier().toString());
flags.set ("JucePlugin_AUCocoaViewClassName", getPluginAUCocoaViewClassName (project).toString());
flags.set ("JucePlugin_RTASCategory", getPluginRTASCategoryCode (project));
flags.set ("JucePlugin_RTASManufacturerCode", "JucePlugin_ManufacturerCode");
flags.set ("JucePlugin_RTASProductId", "JucePlugin_PluginCode");
@@ -476,8 +475,6 @@ namespace AUHelpers
JUCE_AU_PUBLIC "AUBase/AUBase.h",
JUCE_AU_PUBLIC "AUBase/AUDispatch.cpp",
JUCE_AU_PUBLIC "AUBase/AUDispatch.h",
JUCE_AU_PUBLIC "AUBase/AUPlugInDispatch.cpp",
JUCE_AU_PUBLIC "AUBase/AUPlugInDispatch.h",
JUCE_AU_PUBLIC "AUBase/AUInputElement.cpp",
JUCE_AU_PUBLIC "AUBase/AUInputElement.h",
JUCE_AU_PUBLIC "AUBase/AUOutputElement.cpp",


+ 0
- 6
extras/audio plugin demo/Builds/MacOSX/JuceDemoPlugin.xcodeproj/project.pbxproj View File

@@ -33,7 +33,6 @@
64AD03BB3A52EC9E2C030B8E = { isa = PBXBuildFile; fileRef = 0906F59C839873ADC151A188; settings = {COMPILER_FLAGS = "-w"; }; };
7B4173581D4B03969E995CA5 = { isa = PBXBuildFile; fileRef = 4A6A465D7BC825BB91F562C3; settings = {COMPILER_FLAGS = "-w"; }; };
F979285E6636E6CD0669E164 = { isa = PBXBuildFile; fileRef = ED5C52722B9653FA3B009C01; settings = {COMPILER_FLAGS = "-w"; }; };
9DF250E9FBA4779BC6913B60 = { isa = PBXBuildFile; fileRef = 8A413A16A86724E5F72C3A64; settings = {COMPILER_FLAGS = "-w"; }; };
5677D9A2B913014EB1B3E3B3 = { isa = PBXBuildFile; fileRef = 3031159E3E89066250B1D48A; settings = {COMPILER_FLAGS = "-w"; }; };
C74F334B15AD60AC06A086B7 = { isa = PBXBuildFile; fileRef = 534561E5F94B5D7A093485DF; settings = {COMPILER_FLAGS = "-w"; }; };
CB8698D8E9F64AD71C0608E4 = { isa = PBXBuildFile; fileRef = A5985F8B03893AA7EFD0273A; settings = {COMPILER_FLAGS = "-w"; }; };
@@ -217,7 +216,6 @@
29917AAA580F21BF2798D071 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DirectoryContentsDisplayComponent.h"; path = "../../../../modules/juce_gui_basics/filebrowser/juce_DirectoryContentsDisplayComponent.h"; sourceTree = "SOURCE_ROOT"; };
29BA2BABEFBB624A9EEE83F3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Misc.cpp"; path = "../../../../modules/juce_core/native/juce_android_Misc.cpp"; sourceTree = "SOURCE_ROOT"; };
2A073A793701BE742D5D8CA9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioThumbnailBase.h"; path = "../../../../modules/juce_audio_utils/gui/juce_AudioThumbnailBase.h"; sourceTree = "SOURCE_ROOT"; };
2A64B8476158FF7987B18117 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = AUPlugInDispatch.h; path = Extras/CoreAudio/AudioUnits/AUPublic/AUBase/AUPlugInDispatch.h; sourceTree = "DEVELOPER_DIR"; };
2AA4939A70E1E1D6B907DA87 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ImageConvolutionKernel.h"; path = "../../../../modules/juce_graphics/images/juce_ImageConvolutionKernel.h"; sourceTree = "SOURCE_ROOT"; };
2AA92DC1171DAF0BA4BB0E63 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_audio_formats.h"; path = "../../../../modules/juce_audio_formats/juce_audio_formats.h"; sourceTree = "SOURCE_ROOT"; };
2B2D54521D69CF4407471A56 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = AUBase.cpp; path = Extras/CoreAudio/AudioUnits/AUPublic/AUBase/AUBase.cpp; sourceTree = "DEVELOPER_DIR"; };
@@ -544,7 +542,6 @@
8983E39490E8EF99EB09C783 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_Messaging.cpp"; path = "../../../../modules/juce_events/native/juce_linux_Messaging.cpp"; sourceTree = "SOURCE_ROOT"; };
899ACC40FF89BB35307F3BAC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ActionBroadcaster.cpp"; path = "../../../../modules/juce_events/broadcasters/juce_ActionBroadcaster.cpp"; sourceTree = "SOURCE_ROOT"; };
89E2E6BA6057311E66E8A8BF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ArrayAllocationBase.h"; path = "../../../../modules/juce_core/containers/juce_ArrayAllocationBase.h"; sourceTree = "SOURCE_ROOT"; };
8A413A16A86724E5F72C3A64 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = AUPlugInDispatch.cpp; path = Extras/CoreAudio/AudioUnits/AUPublic/AUBase/AUPlugInDispatch.cpp; sourceTree = "DEVELOPER_DIR"; };
8AE97D16E9DA460011B379BA = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DropShadower.h"; path = "../../../../modules/juce_gui_basics/misc/juce_DropShadower.h"; sourceTree = "SOURCE_ROOT"; };
8AEC8C684E53D6E14FC97605 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_win32_HiddenMessageWindow.h"; path = "../../../../modules/juce_events/native/juce_win32_HiddenMessageWindow.h"; sourceTree = "SOURCE_ROOT"; };
8B184C08A51AA14F56E42152 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = AUBase.h; path = Extras/CoreAudio/AudioUnits/AUPublic/AUBase/AUBase.h; sourceTree = "DEVELOPER_DIR"; };
@@ -1936,8 +1933,6 @@
728F423CEB1B9E6CA8545895,
ED5C52722B9653FA3B009C01,
519C6BF83160A6B581905C58,
8A413A16A86724E5F72C3A64,
2A64B8476158FF7987B18117,
4550EB50BD17ADECC674C83C,
3031159E3E89066250B1D48A,
A62DF2CD31D7890E1B54B18C,
@@ -2120,7 +2115,6 @@
64AD03BB3A52EC9E2C030B8E,
7B4173581D4B03969E995CA5,
F979285E6636E6CD0669E164,
9DF250E9FBA4779BC6913B60,
5677D9A2B913014EB1B3E3B3,
C74F334B15AD60AC06A086B7,
CB8698D8E9F64AD71C0608E4,


+ 0
- 3
extras/audio plugin demo/JuceLibraryCode/AppConfig.h View File

@@ -248,9 +248,6 @@
#ifndef JucePlugin_CFBundleIdentifier
#define JucePlugin_CFBundleIdentifier com.rawmaterialsoftware.JuceDemoPlugin
#endif
#ifndef JucePlugin_AUCocoaViewClassName
#define JucePlugin_AUCocoaViewClassName JuceDemoProjectAU_V1
#endif
#ifndef JucePlugin_RTASCategory
#define JucePlugin_RTASCategory ePlugInCategory_None
#endif


+ 147
- 156
modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm View File

@@ -73,7 +73,6 @@
#define Point CarbonDummyPointName
#include "AUCarbonViewBase.h"
#undef Point
class JuceAUView;
#endif
#ifdef __clang__
@@ -89,8 +88,6 @@
#include "../../juce_core/native/juce_osx_ObjCHelpers.h"
//==============================================================================
#define JuceUICreationClass JucePlugin_AUCocoaViewClassName
static Array<void*> activePlugins, activeUIs;
static const AudioUnitPropertyID juceFilterObjectPropertyID = 0x1a45ffe9;
@@ -175,7 +172,16 @@ public:
shutdownJuce_GUI();
}
void deleteActiveEditors();
void deleteActiveEditors()
{
for (int i = activeUIs.size(); --i >= 0;)
{
id ui = (id) activeUIs.getUnchecked(i);
if (JuceUIViewClass::getAU (ui) == this)
JuceUIViewClass::deleteEditor (ui);
}
}
//==============================================================================
ComponentResult GetPropertyInfo (AudioUnitPropertyID inID,
@@ -253,13 +259,14 @@ public:
{
JUCE_AUTORELEASEPOOL
AudioUnitCocoaViewInfo* info = (AudioUnitCocoaViewInfo*) outData;
static JuceUICreationClass cls;
const File bundleFile (File::getSpecialLocation (File::currentApplicationFile));
NSString* bundlePath = [NSString stringWithUTF8String: (const char*) bundleFile.getFullPathName().toUTF8()];
NSBundle* b = [NSBundle bundleWithPath: bundlePath];
info->mCocoaAUViewClass[0] = (CFStringRef) [nsStringLiteral (JUCE_STRINGIFY (JuceUICreationClass)) retain];
AudioUnitCocoaViewInfo* info = static_cast <AudioUnitCocoaViewInfo*> (outData);
info->mCocoaAUViewClass[0] = (CFStringRef) [juceStringToNS (class_getName (cls.cls)) retain];
info->mCocoaAUViewBundleLocation = (CFURLRef) [[NSURL fileURLWithPath: [b bundlePath]] retain];
return noErr;
@@ -841,7 +848,6 @@ public:
return noErr;
}
protected:
OSStatus HandleMidiEvent (UInt8 nStatus, UInt8 inChannel, UInt8 inData1, UInt8 inData2,
#if defined (MAC_OS_X_VERSION_10_5)
UInt32 inStartFrame)
@@ -931,121 +937,101 @@ protected:
[view setNeedsDisplay: YES];
}
private:
//==============================================================================
ScopedPointer<AudioProcessor> juceFilter;
AudioSampleBuffer bufferSpace;
HeapBlock <float*> channels;
MidiBuffer midiEvents, incomingEvents;
bool prepared;
SMPTETime lastSMPTETime;
AUChannelInfo channelInfo [numChannelConfigs];
AudioUnitEvent auEvent;
mutable Array<AUPreset> presetsArray;
CriticalSection incomingMidiLock;
void clearPresetsArray() const
class EditorCompHolder : public Component
{
for (int i = presetsArray.size(); --i >= 0;)
CFRelease (presetsArray.getReference(i).presetName);
presetsArray.clear();
}
public:
EditorCompHolder (AudioProcessorEditor* const editor)
{
setSize (editor->getWidth(), editor->getHeight());
addAndMakeVisible (editor);
JUCE_DECLARE_NON_COPYABLE (JuceAU)
};
#if ! JucePlugin_EditorRequiresKeyboardFocus
setWantsKeyboardFocus (false);
#else
setWantsKeyboardFocus (true);
#endif
}
//==============================================================================
class EditorCompHolder : public Component
{
public:
EditorCompHolder (AudioProcessorEditor* const editor)
{
setSize (editor->getWidth(), editor->getHeight());
addAndMakeVisible (editor);
~EditorCompHolder()
{
deleteAllChildren(); // note that we can't use a ScopedPointer because the editor may
// have been transferred to another parent which takes over ownership.
}
#if ! JucePlugin_EditorRequiresKeyboardFocus
setWantsKeyboardFocus (false);
#else
setWantsKeyboardFocus (true);
#endif
}
static NSView* createViewFor (AudioProcessor* filter, JuceAU* au, AudioProcessorEditor* const editor)
{
EditorCompHolder* editorCompHolder = new EditorCompHolder (editor);
NSRect r = NSMakeRect (0, 0, editorCompHolder->getWidth(), editorCompHolder->getHeight());
~EditorCompHolder()
{
deleteAllChildren(); // note that we can't use a ScopedPointer because the editor may
// have been transferred to another parent which takes over ownership.
}
static JuceUIViewClass cls;
NSView* view = [[cls.createInstance() initWithFrame: r] autorelease];
static NSView* createViewFor (AudioProcessor* filter, JuceAU* au, AudioProcessorEditor* const editor)
{
EditorCompHolder* editorCompHolder = new EditorCompHolder (editor);
NSRect r = NSMakeRect (0, 0, editorCompHolder->getWidth(), editorCompHolder->getHeight());
JuceUIViewClass::setFilter (view, filter);
JuceUIViewClass::setAU (view, au);
JuceUIViewClass::setEditor (view, editorCompHolder);
static JuceUIViewClass cls;
NSView* view = [[cls.createInstance() initWithFrame: r] autorelease];
[view setHidden: NO];
[view setPostsFrameChangedNotifications: YES];
JuceUIViewClass::setFilter (view, filter);
JuceUIViewClass::setAU (view, au);
JuceUIViewClass::setEditor (view, editorCompHolder);
[[NSNotificationCenter defaultCenter] addObserver: view
selector: @selector (applicationWillTerminate:)
name: NSApplicationWillTerminateNotification
object: nil];
activeUIs.add (view);
[view setHidden: NO];
[view setPostsFrameChangedNotifications: YES];
editorCompHolder->addToDesktop (0, (void*) view);
editorCompHolder->setVisible (view);
return view;
}
[[NSNotificationCenter defaultCenter] addObserver: view
selector: @selector (applicationWillTerminate:)
name: NSApplicationWillTerminateNotification
object: nil];
activeUIs.add (view);
void childBoundsChanged (Component*)
{
if (Component* editor = getChildComponent(0))
{
const int w = jmax (32, editor->getWidth());
const int h = jmax (32, editor->getHeight());
editorCompHolder->addToDesktop (0, (void*) view);
editorCompHolder->setVisible (view);
return view;
}
if (getWidth() != w || getHeight() != h)
setSize (w, h);
void childBoundsChanged (Component*)
{
if (Component* editor = getChildComponent(0))
{
const int w = jmax (32, editor->getWidth());
const int h = jmax (32, editor->getHeight());
if (getWidth() != w || getHeight() != h)
setSize (w, h);
NSView* view = (NSView*) getWindowHandle();
NSRect r = [[view superview] frame];
r.size.width = editor->getWidth();
r.size.height = editor->getHeight();
[[view superview] setFrame: r];
[view setFrame: NSMakeRect (0, 0, editor->getWidth(), editor->getHeight())];
[view setNeedsDisplay: YES];
NSView* view = (NSView*) getWindowHandle();
NSRect r = [[view superview] frame];
r.size.width = editor->getWidth();
r.size.height = editor->getHeight();
[[view superview] setFrame: r];
[view setFrame: NSMakeRect (0, 0, editor->getWidth(), editor->getHeight())];
[view setNeedsDisplay: YES];
}
}
}
bool keyPressed (const KeyPress&)
{
if (PluginHostType().isAbletonLive())
bool keyPressed (const KeyPress&)
{
static NSTimeInterval lastEventTime = 0; // check we're not recursively sending the same event
NSTimeInterval eventTime = [[NSApp currentEvent] timestamp];
if (lastEventTime != eventTime)
if (PluginHostType().isAbletonLive())
{
lastEventTime = eventTime;
static NSTimeInterval lastEventTime = 0; // check we're not recursively sending the same event
NSTimeInterval eventTime = [[NSApp currentEvent] timestamp];
NSView* view = (NSView*) getWindowHandle();
NSView* hostView = [view superview];
NSWindow* hostWindow = [hostView window];
if (lastEventTime != eventTime)
{
lastEventTime = eventTime;
[hostWindow makeFirstResponder: hostView];
[hostView keyDown: [NSApp currentEvent]];
[hostWindow makeFirstResponder: view];
NSView* view = (NSView*) getWindowHandle();
NSView* hostView = [view superview];
NSWindow* hostWindow = [hostView window];
[hostWindow makeFirstResponder: hostView];
[hostView keyDown: [NSApp currentEvent]];
[hostWindow makeFirstResponder: view];
}
}
return false;
}
return false;
}
private:
JUCE_DECLARE_NON_COPYABLE (EditorCompHolder)
};
//==============================================================================
struct JuceUIViewClass : public ObjCClass <NSView>
@@ -1135,76 +1121,73 @@ public:
}
};
private:
JUCE_DECLARE_NON_COPYABLE (EditorCompHolder)
};
void JuceAU::deleteActiveEditors()
{
for (int i = activeUIs.size(); --i >= 0;)
//==============================================================================
struct JuceUICreationClass : public ObjCClass <NSObject>
{
id ui = (id) activeUIs.getUnchecked(i);
if (EditorCompHolder::JuceUIViewClass::getAU (ui) == this)
EditorCompHolder::JuceUIViewClass::deleteEditor (ui);
}
}
JuceUICreationClass() : ObjCClass <NSObject> ("JUCE_AUCocoaViewClass_")
{
addMethod (@selector (interfaceVersion), interfaceVersion, @encode (unsigned int), "@:");
addMethod (@selector (description), description, @encode (NSString*), "@:");
addMethod (@selector (uiViewForAudioUnit:withSize:), uiViewForAudioUnit, @encode (NSView*), "@:", @encode (AudioUnit), @encode (NSSize));
//==============================================================================
@interface JuceUICreationClass : NSObject <AUCocoaUIBase>
{
}
addProtocol (@protocol (AUCocoaUIBase));
- (unsigned) interfaceVersion;
- (NSString*) description;
- (NSView*) uiViewForAudioUnit: (AudioUnit) inAudioUnit
withSize: (NSSize) inPreferredSize;
@end
registerClass();
}
@implementation JuceUICreationClass
private:
static unsigned int interfaceVersion (id, SEL) { return 0; }
- (unsigned) interfaceVersion
{
return 0;
}
static NSString* description (id, SEL)
{
return [NSString stringWithString: nsStringLiteral (JucePlugin_Name)];
}
- (NSString*) description
{
return [NSString stringWithString: nsStringLiteral (JucePlugin_Name)];
}
static NSView* uiViewForAudioUnit (id, SEL, AudioUnit inAudioUnit, NSSize)
{
void* pointers[2];
UInt32 propertySize = sizeof (pointers);
- (NSView*) uiViewForAudioUnit: (AudioUnit) inAudioUnit
withSize: (NSSize) inPreferredSize
{
void* pointers[2];
UInt32 propertySize = sizeof (pointers);
if (AudioUnitGetProperty (inAudioUnit, juceFilterObjectPropertyID,
kAudioUnitScope_Global, 0, pointers, &propertySize) == noErr)
{
if (AudioProcessor* filter = static_cast <AudioProcessor*> (pointers[0]))
if (AudioProcessorEditor* editorComp = filter->createEditorIfNeeded())
return EditorCompHolder::createViewFor (filter, static_cast <JuceAU*> (pointers[1]), editorComp);
}
if (AudioUnitGetProperty (inAudioUnit,
juceFilterObjectPropertyID,
kAudioUnitScope_Global,
0,
pointers,
&propertySize) != noErr)
return nil;
return nil;
}
};
AudioProcessor* filter = (AudioProcessor*) pointers[0];
JuceAU* au = (JuceAU*) pointers[1];
private:
//==============================================================================
ScopedPointer<AudioProcessor> juceFilter;
AudioSampleBuffer bufferSpace;
HeapBlock <float*> channels;
MidiBuffer midiEvents, incomingEvents;
bool prepared;
SMPTETime lastSMPTETime;
AUChannelInfo channelInfo [numChannelConfigs];
AudioUnitEvent auEvent;
mutable Array<AUPreset> presetsArray;
CriticalSection incomingMidiLock;
if (filter == nullptr)
return nil;
void clearPresetsArray() const
{
for (int i = presetsArray.size(); --i >= 0;)
CFRelease (presetsArray.getReference(i).presetName);
AudioProcessorEditor* editorComp = filter->createEditorIfNeeded();
if (editorComp == nullptr)
return nil;
presetsArray.clear();
}
return EditorCompHolder::createViewFor (filter, au, editorComp);
}
@end
JUCE_DECLARE_NON_COPYABLE (JuceAU)
};
//==============================================================================
#if BUILD_AU_CARBON_UI
//==============================================================================
class JuceAUView : public AUCarbonViewBase
{
public:
@@ -1278,7 +1261,7 @@ private:
aren't so careful) */
jassert (Component::getCurrentlyModalComponent() == nullptr);
if (EditorCompHolder* editorCompHolder = dynamic_cast <EditorCompHolder*> (windowComp->getChildComponent(0)))
if (JuceAU::EditorCompHolder* editorCompHolder = dynamic_cast <JuceAU::EditorCompHolder*> (windowComp->getChildComponent(0)))
if (AudioProcessorEditor* audioProcessEditor = dynamic_cast <AudioProcessorEditor*> (editorCompHolder->getChildComponent(0)))
juceFilter->editorBeingDeleted (audioProcessEditor);
@@ -1424,7 +1407,7 @@ private:
private:
HIViewRef parentView;
NSWindow* hostWindow;
EditorCompHolder editor;
JuceAU::EditorCompHolder editor;
bool recursive;
};
};
@@ -1458,6 +1441,10 @@ private:
//==============================================================================
JUCE_COMPONENT_ENTRY (JuceAU, JucePlugin_AUExportPrefix, Entry)
#ifndef AUDIOCOMPONENT_ENTRY
#define JUCE_DISABLE_AU_FACTORY_ENTRY 1
#endif
#if ! JUCE_DISABLE_AU_FACTORY_ENTRY // (You might need to disable this for old Xcode 3 builds)
JUCE_FACTORY_ENTRY (JuceAU, JucePlugin_AUExportPrefix)
#endif
@@ -1466,4 +1453,8 @@ JUCE_FACTORY_ENTRY (JuceAU, JucePlugin_AUExportPrefix)
JUCE_COMPONENT_ENTRY (JuceAUView, JucePlugin_AUExportPrefix, ViewEntry)
#endif
#if ! JUCE_DISABLE_AU_FACTORY_ENTRY
#include "AUPlugInDispatch.cpp"
#endif
#endif

+ 0
- 4
modules/juce_audio_plugin_client/utility/juce_CheckSettingMacros.h View File

@@ -86,10 +86,6 @@
#error "You need to define the JucePlugin_WinBag_path value!"
#endif
#if JucePlugin_Build_AU && ! defined (JucePlugin_AUCocoaViewClassName)
#error "You need to define the JucePlugin_AUCocoaViewClassName value!"
#endif
#if JucePlugin_Build_LV2 && ! defined (JucePlugin_LV2URI)
#error "You need to define the JucePlugin_LV2URI value!"
#endif


Loading…
Cancel
Save