Monday, October 8, 2012

Creating Advanced Alert Window class: Part 36

In this tutorial we'll add the ability to turn on a feature that stretches the alert windor horizontally, if its height has reached max height and text is out of window bounds.

Go to AdvAlertWindow class, find the autoSize() function.

Here we'll add an if...statement that checks if horizontalStretch feature is turned on (by checking if horizontalStretch boolean is true; we'll add that variable soon) and if height equals maxHeight and if textHeight is greater than textField's height.

If all those values are true, then we increase the width by, say, 30 pixels. Decrease the position of the window on the x axirs by half of that. Then call autoSize() again.

private function autoSize():void {
var textHeight:int = textField.textHeight;
h = headerHeight + headerPadding.top + headerPadding.bottom + buttonbarHeight + buttonbarPadding.top + buttonbarPadding.bottom + textPadding.top + textPadding.bottom + textHeight + 10;
if (h < minHeight) h = minHeight;
if (h > maxHeight) h = maxHeight;
pos.y = parh / 2 - h / 2;
updateDraw();
if (horizontalStretch && h == maxHeight && textHeight > textField.height) {
w += 30;
pos.x -= 15;
autoSize();
}
}

Now declare the horizontalStretch variable in the beginning:

private var horizontalStretch:Boolean;

Edit the constructor so that it receives a hstretch value and applies its value to horizontalStretch:

public function AdvAlertWindow(pt:String, ptt:String, pw:int, ph:int, ppos:Point, sk:AdvAlertSkin, bt:Array, defaultButtonSkin:AdvAlertButtonSkin, parentw:int, parenth:int, mheight:int, maxheight:int, hstretch:Boolean) 
{
horizontalStretch = hstretch;
minHeight = mheight;
maxHeight = maxheight;
t = pt;
tt = ptt;
w = pw;
h = ph;
pos = ppos;
buttons = bt;
parw = parentw;
parh = parenth;

setSkin(sk);

textField = new TextField();
addChild(textField);

titleField = new TextField();
addChild(titleField);
for (var i:int = 0; i < buttons.length; i++) {
if (buttons[i].currentSkin == null) {
buttons[i].setSkin(defaultButtonSkin);
buttons[i].updateDraw();
}
addChild(buttons[i]);
}

updateDraw();

if (ph == 0) autoSize();
// add dynamic content
addContent();
}

Full AdvAlertWindow class so far:

package com.kircode.AdvAlert 
{
import flash.display.DisplayObject;
import flash.display.MovieClip;
import flash.events.Event;
import flash.geom.Point;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.display.GradientType;

/**
 * Advanced Alert window object.
 * @author Kirill Poletaev
 */
public class AdvAlertWindow extends MovieClip
{
private var t:String;
private var tt:String;
private var w:int;
private var h:int;
private var pos:Point;
private var parw:int;
private var parh:int;
public var buttons:Array;
public var content:Array;

private var textField:TextField;
private var titleField:TextField;

private var textPadding:TextPadding;
private var headerPadding:TextPadding;
private var titlePadding:TextPadding;
private var headerHeight:Number;
private var buttonbarPadding:TextPadding;
private var buttonbarHeight:Number;
private var titleFormat:TextFormat;
private var textFormat:TextFormat;
private var selectable:Boolean;
private var headerRect:*;
private var bgRect:*;
private var headerStroke:*;
private var bgStroke:*;
private var skinFilters:Array;
private var buttonInterval:Number;
private var defaultButtonSkin:AdvAlertButtonSkin;
private var buttonbarAlign:String;
private var dynamicContent:Array;
private var minHeight:int;
private var maxHeight:int;
private var textFilters:Array;
private var titleFilters:Array;
private var horizontalStretch:Boolean;

public function AdvAlertWindow(pt:String, ptt:String, pw:int, ph:int, ppos:Point, sk:AdvAlertSkin, bt:Array, defaultButtonSkin:AdvAlertButtonSkin, parentw:int, parenth:int, mheight:int, maxheight:int, hstretch:Boolean) 
{
horizontalStretch = hstretch;
minHeight = mheight;
maxHeight = maxheight;
t = pt;
tt = ptt;
w = pw;
h = ph;
pos = ppos;
buttons = bt;
parw = parentw;
parh = parenth;

setSkin(sk);

textField = new TextField();
addChild(textField);

titleField = new TextField();
addChild(titleField);
for (var i:int = 0; i < buttons.length; i++) {
if (buttons[i].currentSkin == null) {
buttons[i].setSkin(defaultButtonSkin);
buttons[i].updateDraw();
}
addChild(buttons[i]);
}

updateDraw();

if (ph == 0) autoSize();
// add dynamic content
addContent();
}

public function setSkin(skin:AdvAlertSkin):void {
textPadding = skin.textPadding;
headerPadding = skin.headerPadding;
titlePadding = skin.titlePadding;
headerHeight = skin.headerHeight;
titleFormat = skin.titleFormat;
textFormat = skin.textFormat;
selectable = skin.selectable;
bgRect = skin.bgRect;
headerRect = skin.headerRect;
bgStroke = skin.bgStroke;
headerStroke = skin.headerStroke;
skinFilters = skin.filters;
buttonbarHeight = skin.buttonbarHeight;
buttonbarPadding = skin.buttonbarPadding;
buttonInterval = skin.buttonInterval;
buttonbarAlign = skin.buttonbarAlign;
dynamicContent = skin.dynamicContent;
textFilters = skin.textFilters;
titleFilters = skin.titleFilters;
}

private function autoSize():void {
var textHeight:int = textField.textHeight;
h = headerHeight + headerPadding.top + headerPadding.bottom + buttonbarHeight + buttonbarPadding.top + buttonbarPadding.bottom + textPadding.top + textPadding.bottom + textHeight + 10;
if (h < minHeight) h = minHeight;
if (h > maxHeight) h = maxHeight;
pos.y = parh / 2 - h / 2;
updateDraw();
if (horizontalStretch && h == maxHeight && textHeight > textField.height) {
w += 30;
pos.x -= 15;
autoSize();
}
}

private function addContent():void {
content = [];
for (var i:int = 0; i < dynamicContent.length; i++) {
var object:DisplayObject = new dynamicContent[i].obj();
addChild(object);
object.x = dynamicContent[i].pos.x + pos.x;
object.y = dynamicContent[i].pos.y + pos.y;
content.push(object);
}
}

public function updateDraw():void {
if (h < minHeight) h = minHeight;
if (h > maxHeight) h = maxHeight;

this.graphics.clear();
// filters
this.filters = skinFilters;

// bg stroke
if (bgStroke is SolidColorStroke) {
this.graphics.lineStyle(bgStroke._lineThickness, bgStroke._lineColor, bgStroke._lineAlpha);
}
if (bgStroke is GradientColorStroke) {
this.graphics.lineStyle(bgStroke._lineThickness);
this.graphics.lineGradientStyle(bgStroke._gradientType, bgStroke._colors, bgStroke._alphas, bgStroke._ratios, bgStroke._matrix, bgStroke._spreadMethod, bgStroke._interpolationMethod, bgStroke._focalPointRatio);
}
if (bgStroke is BitmapStroke) {
this.graphics.lineStyle(bgStroke._lineThickness);
this.graphics.lineBitmapStyle(bgStroke._bitmap, bgStroke._matrix, bgStroke._repeat, bgStroke._smooth);
}

// bg fill
if (bgRect is SolidColorRect) this.graphics.beginFill(bgRect._backgroundColor, bgRect._alpha);
if (bgRect is GradientColorRect) this.graphics.beginGradientFill(bgRect._gradientType, bgRect._colors, bgRect._alphas, bgRect._ratios, bgRect._matrix, bgRect._spreadMethod, bgRect._interpolationMethod, bgRect._focalPointRatio);
if (bgRect is BitmapRect) this.graphics.beginBitmapFill(bgRect._bitmap, bgRect._matrix, bgRect._repeat, bgRect._smooth);

this.graphics.drawRoundRectComplex(pos.x, pos.y, w, h,
bgRect._radius[0], bgRect._radius[1], bgRect._radius[2], bgRect._radius[3]);
this.graphics.endFill();

// header stroke
if (headerStroke is SolidColorStroke) {
this.graphics.lineStyle(headerStroke._lineThickness, headerStroke._lineColor, headerStroke._lineAlpha);
}
if (headerStroke is GradientColorStroke) {
this.graphics.lineStyle(headerStroke._lineThickness);
this.graphics.lineGradientStyle(headerStroke._gradientType, headerStroke._colors, headerStroke._alphas, headerStroke._ratios, headerStroke._matrix, headerStroke._spreadMethod, headerStroke._interpolationMethod, headerStroke._focalPointRatio);
}
if (headerStroke is BitmapStroke) {
this.graphics.lineStyle(headerStroke._lineThickness);
this.graphics.lineBitmapStyle(headerStroke._bitmap, headerStroke._matrix, headerStroke._repeat, headerStroke._smooth);
}

// header fill
if (headerRect is SolidColorRect) this.graphics.beginFill(headerRect._backgroundColor, headerRect._alpha);
if (headerRect is GradientColorRect) this.graphics.beginGradientFill(headerRect._gradientType, headerRect._colors, headerRect._alphas, headerRect._ratios, headerRect._matrix, headerRect._spreadMethod, headerRect._interpolationMethod, headerRect._focalPointRatio);
if (headerRect is BitmapRect) this.graphics.beginBitmapFill(headerRect._bitmap, headerRect._matrix, headerRect._repeat, headerRect._smooth);

this.graphics.drawRoundRectComplex(pos.x + headerPadding.left, pos.y + headerPadding.top, w - (headerPadding.left + headerPadding.right), headerHeight,
headerRect._radius[0], headerRect._radius[1], headerRect._radius[2], headerRect._radius[3]);
this.graphics.endFill();

// title
titleField.width = w - (headerPadding.left + headerPadding.right);
titleField.text = tt;
titleField.height = headerHeight;
titleField.x = pos.x + headerPadding.left + titlePadding.left;
titleField.y = pos.y + headerPadding.top + titlePadding.top;

// text
textField.width = w - (textPadding.right + textPadding.left);
textField.height = h - (textPadding.top + textPadding.bottom + headerPadding.top + headerPadding.bottom + headerHeight + buttonbarHeight + buttonbarPadding.top + buttonbarPadding.bottom);
textField.text = t;
textField.x = pos.x + textPadding.left;
textField.y = pos.y + textPadding.top + headerHeight + headerPadding.bottom + headerPadding.top;

// formats
textField.setTextFormat(textFormat);
titleField.setTextFormat(titleFormat);
textField.selectable = selectable;
titleField.selectable = selectable;
textField.multiline = true;
textField.wordWrap = true;
titleField.filters = titleFilters;
textField.filters = textFilters;

// buttons
var xCoord:Number;
var i:int

// right align
if(buttonbarAlign=="right"){
if (buttons.length > 0) xCoord = pos.x + w - buttons[buttons.length-1].w - buttonbarPadding.right;
for (i = buttons.length-1; i >= 0; i--) {
if (i != buttons.length-1) xCoord -= buttons[i].w + buttonInterval;
buttons[i].x = xCoord;
buttons[i].y = pos.y + h - buttons[i].h - buttonbarPadding.bottom;
}
}

// left align
if(buttonbarAlign=="left"){
if (buttons.length > 0) xCoord = pos.x + buttonbarPadding.left;
for (i = 0; i < buttons.length; i++) {
if (i > 0) xCoord += buttons[i].w + buttonInterval;
buttons[i].x = xCoord;
buttons[i].y = pos.y + h - buttons[i].h - buttonbarPadding.bottom;
}
}

// center align
if(buttonbarAlign=="center"){
if (buttons.length > 0) {
var buttonbarWidth:Number = 0;
for (i = 0; i < buttons.length; i++) {
buttonbarWidth += buttons[i].w + buttonInterval;
}
buttonbarWidth -= buttonInterval;
xCoord = pos.x + (w/2) - buttonbarWidth/2;
}
for (i = 0; i < buttons.length; i++) {
if (i > 0) xCoord += buttons[i].w + buttonInterval;
buttons[i].x = xCoord;
buttons[i].y = pos.y + h - buttons[i].h - buttonbarPadding.bottom;
}
}
}


}

}

Now let's go one level up and into the AdvAlertManager class. Here, we update the alert() function so that it has one more parameter, which we will then use to pass a value to AdvAlertWindow object that is created:

/**
 * Create an alert window.
 * @paramtext Text value of the alert window.
 * @paramtitle Title value of the alert window.
 * @parambuttons (Optional) Array of AdvAlertButton objects that represent buttons in the alert window.
 * @paramcloseHandler (Optional) Close handling function. Must receive a string value as parameter (which will hold the label of the button that was clicked).
 * @paramwidth (Optional) Width of the alert window. If 0, set to default.
 * @paramheight (Optional) Height of the alert window. If 0, set to default. If default is 0, window will be auto sized.
 * @paramposition (Optional) Coordinates of top-left corner of the alert window. If not specified - the window is centered.
 * @paramminHeight (Optional) The minimum height value of the alert window. If 0, set to default.
 * @parammaxHeight (Optional) The maximum height value of the alert window. If 0, set to default.
 * @paramhorizontalStretch (Optional) If set to true, will stretch the window horizontally if the text doesn't fit and the height has already reached maxHeight.
 * @paramopenSound (Optional) Sound to play when the window opens. Specified default sound plays by default.
 */

public function alert(text:String, title:String = "", buttons:Array = null, closeHandler:Function = null, width:int = 0, height:int = 0, position:Point = null, minHeight:int = 0, maxHeight:int = 0, horizontalStretch:Boolean = true, openSound:Sound = null):AdvAlertWindow {
if (width == 0) width = defWidth;
if (height == 0) height = defHeight;
if (minHeight == 0) minHeight = defMinHeight;
if (maxHeight == 0) maxHeight = defMaxHeight;
if (openSound != null) {
openSound.play();
}else
if (openingSound != null) {
openingSound.play();
}
if (position == null) position = new Point((pWidth / 2) - (width / 2), (pHeight / 2) - (height / 2));
if (buttons == null) buttons = [new AdvAlertButton("OK")];
for (var i:int = buttons.length - 1; i >= 0; i--) {
if (!buttons[i] is AdvAlertButton) {
throw new Error("An item in 'buttons' array is not an AdvAlertButton instance. Ignoring...");
buttons.splice(i, 1);
}else {
buttons[i].addEventListener(MouseEvent.CLICK, buttonHandler);
}
}
var w:AdvAlertWindow = new AdvAlertWindow(text, title, width, height, position, skin, buttons, bSkin, pWidth, pHeight, minHeight, maxHeight, horizontalStretch);
var b:AdvAlertBlur = new AdvAlertBlur(pWidth, pHeight, skin);
var currentIndex:int = windows.length;
windows.push( {window:w, blur:b, onclose:closeHandler} );
defaultContainer.addChild(b);
defaultContainer.addChild(w);

topWindow = w;
focusedButton = -1;
if (tabDisable) {
defaultContainer.tabChildren = false;
defaultContainer.tabEnabled = false;
}

function buttonHandler(evt:MouseEvent):void {
closeWindow(currentIndex, closeHandler);
}
return w;
}

Full code so far:

package com.kircode.AdvAlert 
{
import flash.display.DisplayObjectContainer;
import flash.display.MovieClip;
import flash.display.Stage;
import flash.events.MouseEvent;
import flash.geom.Point;
import flash.events.KeyboardEvent;
import flash.events.Event;
import flash.media.Sound;
/**
 * Advanced Alert window manager.
 * @author Kirill Poletaev
 */
public class AdvAlertManager 
{
private var windows:Array;
private var defaultContainer:DisplayObjectContainer;
private var pWidth:int;
private var pHeight:int;
private var skin:AdvAlertSkin;
private var bSkin:AdvAlertButtonSkin;
private var tabDisable:Boolean;
private var topWindow:AdvAlertWindow;
private var focusedButton:int;
private var stg:Stage;
private var openingSound:Sound;
private var defWidth:int;
private var defHeight:int;
private var defMinHeight:int;
private var defMaxHeight:int;

/**
 * AdvAlertManager constructor.
 * @paramdefaultWindowContainer Parent container of alert windows.
 * @paramstage Stage reference.
 * @paramwindowSkin Default skin for alert windows.
 * @parambuttonSkin Default skin for buttons in this window.
 * @paramopenSound Default window open sound.
 * @paramdisableTab Disable tab focusing on the parent container when an alert window is visible. This also enables tab navigation for the alert window's buttons.
 * @paramdefaultWidth Set default width for all window alerts. 300 by default.
 * @paramdefaultHeight Set default height for all window alerts. 0 by default. If 0, the window is auto-sized based on the text length.
 * @paramdefaultMinHeight Set default minimal height for all window alerts. 0 by default.
 * @paramdefaultMaxHeight Set default maximal height for all window alerts. 0 by default. If 0, automatically set to the height of the stage.
 */

public function AdvAlertManager(defaultWindowContainer:DisplayObjectContainer, stage:Stage, windowSkin:AdvAlertSkin = null, buttonSkin:AdvAlertButtonSkin = null, openSound:Sound = null, disableTab:Boolean = true, defaultWidth:int = 300, defaultHeight:int = 0, defaultMinHeight:int = 0, defaultMaxHeight:int = 0) 
{
defWidth = defaultWidth;
defHeight = defaultHeight;
defMinHeight = defaultMinHeight;
defMaxHeight = defaultMaxHeight;
if (defMaxHeight == 0) defMaxHeight = stage.stageHeight;
openingSound = openSound;
skin = windowSkin;
bSkin = buttonSkin;
tabDisable = disableTab;
if (skin == null) skin = new AdvAlertSkin();
if (bSkin == null) bSkin = new AdvAlertButtonSkin();
defaultContainer = defaultWindowContainer;
pWidth = stage.stageWidth;
pHeight = stage.stageHeight;
stg = stage;
windows = [];
stg.addEventListener(KeyboardEvent.KEY_DOWN, kDown);
}

private function kDown(evt:KeyboardEvent):void {
if(tabDisable){
if (evt.keyCode == 9 && topWindow!=null && topWindow.buttons.length>0) {
focusedButton++;
if (focusedButton == topWindow.buttons.length) {
focusedButton = 0;
}
for (var i:int = 0; i < topWindow.buttons.length; i++) {
topWindow.buttons[i].over = false;
topWindow.buttons[i].updateDraw();
}
topWindow.buttons[focusedButton].over = true;
topWindow.buttons[focusedButton].updateDraw();
}
if (evt.keyCode == 13 && topWindow != null && topWindow.buttons.length > 0) {
if (focusedButton == -1) {
focusedButton = 0;
}
var currentIndex:int = windows.length - 1;
var closeHandler:Function = windows[currentIndex].onclose;
closeWindow(currentIndex, closeHandler);
}
}
}

/**
 * Close the top alert window.
 */

public function closeLast():void {
var currentIndex:int = windows.length - 1;
var closeHandler:Function = windows[currentIndex].onclose;
closeWindow(currentIndex, closeHandler);
}

private function closeWindow(currentIndex:int, closeHandler:Function):void {
if (closeHandler != null) closeHandler.call(topWindow.buttons[focusedButton], topWindow.buttons[focusedButton].txt);
windows[currentIndex].window.parent.removeChild(windows[currentIndex].window);
windows[currentIndex].blur.parent.removeChild(windows[currentIndex].blur);
windows.splice(currentIndex, 1);
focusedButton = -1;
if (windows.length > 0) topWindow = windows[windows.length - 1].window;
if (windows.length == 0) topWindow = null;
if (tabDisable && windows.length==0) {
defaultContainer.tabChildren = true;
defaultContainer.tabEnabled = true;
}
}

/**
 * Set skin of all future created alert windows.
 * @paramwindowSkin Reference to an AdvAlertSkin object.
 */

public function setSkin(windowSkin:AdvAlertSkin):void {
skin = windowSkin;
}

/**
 * Set skin of all buttons of future created alert windows.
 * @parambuttonSkin Reference to an AdvAlertButtonSkin object.
 */

public function setButtonSkin(buttonSkin:AdvAlertButtonSkin):void {
bSkin = buttonSkin;
}

/**
 * Set new options for the alert manager object - has the same parameters as the constructor.
 * @paramdefaultWindowContainer Parent container of alert windows.
 * @paramstage Stage reference.
 * @paramwindowSkin Default skin for alert windows.
 * @parambuttonSkin Default skin for buttons in this window.
 * @paramopenSound Default window open sound.
 * @paramdisableTab Disable tab focusing on the parent container when an alert window is visible. This also enables tab navigation for the alert window's buttons.
 * @paramdefaultWidth Set default width for all window alerts. 300 by default.
 * @paramdefaultHeight Set default height for all window alerts. 0 by default. If 0, the window is auto-sized based on the text length.
 * @paramdefaultMinHeight Set default minimal height for all window alerts. 0 by default.
 * @paramdefaultMaxHeight Set default maximal height for all window alerts. 0 by default. If 0, automatically set to the height of the parent.
 */

public function reset(defaultWindowContainer:DisplayObjectContainer, stage:Stage, windowSkin:AdvAlertSkin = null, buttonSkin:AdvAlertButtonSkin = null, openSound:Sound = null, disableTab:Boolean = true, defaultWidth:int = 300, defaultHeight:int = 0, defaultMinHeight:int = 0, defaultMaxHeight:int = 0) 
{
defWidth = defaultWidth;
defHeight = defaultHeight;
defMinHeight = defaultMinHeight;
defMaxHeight = defaultMaxHeight;
if (defMaxHeight == 0) defMaxHeight = stage.stageHeight;
openingSound = openSound;
skin = windowSkin;
bSkin = buttonSkin;
tabDisable = disableTab;
if (skin == null) skin = new AdvAlertSkin();
if (bSkin == null) bSkin = new AdvAlertButtonSkin();
defaultContainer = defaultWindowContainer;
pWidth = stage.stageWidth;
pHeight = stage.stageHeight;
stg = stage;
}

/**
 * Create an alert window.
 * @paramtext Text value of the alert window.
 * @paramtitle Title value of the alert window.
 * @parambuttons (Optional) Array of AdvAlertButton objects that represent buttons in the alert window.
 * @paramcloseHandler (Optional) Close handling function. Must receive a string value as parameter (which will hold the label of the button that was clicked).
 * @paramwidth (Optional) Width of the alert window. If 0, set to default.
 * @paramheight (Optional) Height of the alert window. If 0, set to default. If default is 0, window will be auto sized.
 * @paramposition (Optional) Coordinates of top-left corner of the alert window. If not specified - the window is centered.
 * @paramminHeight (Optional) The minimum height value of the alert window. If 0, set to default.
 * @parammaxHeight (Optional) The maximum height value of the alert window. If 0, set to default.
 * @paramhorizontalStretch (Optional) If set to true, will stretch the window horizontally if the text doesn't fit and the height has already reached maxHeight.
 * @paramopenSound (Optional) Sound to play when the window opens. Specified default sound plays by default.
 */

public function alert(text:String, title:String = "", buttons:Array = null, closeHandler:Function = null, width:int = 0, height:int = 0, position:Point = null, minHeight:int = 0, maxHeight:int = 0, horizontalStretch:Boolean = true, openSound:Sound = null):AdvAlertWindow {
if (width == 0) width = defWidth;
if (height == 0) height = defHeight;
if (minHeight == 0) minHeight = defMinHeight;
if (maxHeight == 0) maxHeight = defMaxHeight;
if (openSound != null) {
openSound.play();
}else
if (openingSound != null) {
openingSound.play();
}
if (position == null) position = new Point((pWidth / 2) - (width / 2), (pHeight / 2) - (height / 2));
if (buttons == null) buttons = [new AdvAlertButton("OK")];
for (var i:int = buttons.length - 1; i >= 0; i--) {
if (!buttons[i] is AdvAlertButton) {
throw new Error("An item in 'buttons' array is not an AdvAlertButton instance. Ignoring...");
buttons.splice(i, 1);
}else {
buttons[i].addEventListener(MouseEvent.CLICK, buttonHandler);
}
}
var w:AdvAlertWindow = new AdvAlertWindow(text, title, width, height, position, skin, buttons, bSkin, pWidth, pHeight, minHeight, maxHeight, horizontalStretch);
var b:AdvAlertBlur = new AdvAlertBlur(pWidth, pHeight, skin);
var currentIndex:int = windows.length;
windows.push( {window:w, blur:b, onclose:closeHandler} );
defaultContainer.addChild(b);
defaultContainer.addChild(w);

topWindow = w;
focusedButton = -1;
if (tabDisable) {
defaultContainer.tabChildren = false;
defaultContainer.tabEnabled = false;
}

function buttonHandler(evt:MouseEvent):void {
closeWindow(currentIndex, closeHandler);
}
return w;
}

}

}

Now, if you go to main.as and create an alert() window with really long text (long enough to hit the height limit), you'll see that the window stretches so that all the text is visible.

That's all for today.

Thanks for reading!

No comments:

Post a Comment