Alloy で AttributedString を使えるようにする

  • 1
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Titanium SDK 3.2.0.GA から追加された AttributedString を Alloy のビューとスタイルのみで使えるようにする、Alloy 自体の改造になります。

Titanium SDK 3.6 系から AttributedString が Android でもサポートされ、ネームスペースも Ti.UI.iOS.AttributedString から Ti.UI.AttributedString へ変更されるため、Alloy 自体の対応は 1.6 系からとなるようです(待てません。というか、諸事情で SDK と Alloy のバージョンを動かせない場合もありますし)。

そもそもコントローラに Ti.UI.iOS.createAttributedStinrg を直書きすれば良いのですが、Alloy を使っていて今更そんなこともしたくないですし。。。

というわけで、Alloy のコンパイラに手を入れて、ビューとスタイルのみで AttributedString を使えるようにしたいというわけです。実際にこれを適用すると、ビューとスタイルは以下のように記述できるようになります。

index.xml

<Alloy>
    <Window>
        <Label>
            <AttributedString class="as">
                Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
            </AttributedString>
        </Label>
        <TextField>
            <AttributedString class="as">
                Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
            </AttributedString>
            <AttributedHintText class="as">
                Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
            </AttributedHintText>
        </TextField>
        <TextArea>
            <AttributedString class="as">
                Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
            </AttributedString>
        </TextArea>
    </Window>
</Alloy>

index.tss

"Window": {
    backgroundColor: '#fff',
    layout: 'vertical'
},
"Label": {
    top: 20,
    right: 10,
    left: 10,
    width: Ti.UI.FILL,
    height: Ti.UI.SIZE
},
"TextField": {
    top: 20,
    right: 10,
    left: 10,
    width: Ti.UI.FILL,
    height: 44
},
"TextArea": {
    top: 20,
    right: 10,
    left: 10,
    width: Ti.UI.FILL,
    height: 200
},
".as": {
    attributes: [
        {
            type: Ti.UI.ATTRIBUTE_UNDERLINES_STYLE,
            value: Ti.UI.ATTRIBUTE_UNDERLINE_STYLE_SINGLE,
            range: [0, 107]
        },
        {
            type: Ti.UI.ATTRIBUTE_BACKGROUND_COLOR,
            value: "red",
            range: [18, 12]
        },
        {
            type: Ti.UI.ATTRIBUTE_BACKGROUND_COLOR,
            value: "blue",
            range: [31, 2]
        },
        {
            type: Ti.UI.ATTRIBUTE_BACKGROUND_COLOR,
            value: "yellow",
            range: [40, 6]
        },
        {
            type: Ti.UI.ATTRIBUTE_FOREGROUND_COLOR,
            value: "orange",
            range: [0, 107]
        },
        {
            type: Ti.UI.ATTRIBUTE_FOREGROUND_COLOR,
            value: "black",
            range: [40, 6]
        }
    ]
}

それではインストールされた Alloy のフォルダへ移動して、以下の改造を適用していきます。

alloy/Alloy/common/constants.js

186 行目にある AdView の下辺りに、以下の 1 行を挿入します。

AttributedString: NS_TI_UI_IOS,

alloy/Alloy/commands/compile/parsers/Ti.UI.iOS.AttributedString

AttributedString 用に以下のコードを新規に設置します。

var _ = require('../../../lib/alloy/underscore'),
    styler = require('../styler'),
    U = require('../../../utils'),
    CU = require('../compilerUtils'),
    tiapp = require('../../../tiapp');

var MIN_VERSION = '3.2.0';

exports.parse = function(node, state) {
    return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
    if (tiapp.version.lt(tiapp.getSdkVersion(), MIN_VERSION)) {
        U.die('Ti.UI.iOS.AttributedString (line ' + node.lineNumber + ') requires Titanium 3.2.0+');
    }

    // Get label text from node text, if present
    var nodeText = U.XML.getNodeText(node);
    if (nodeText) {
        if (U.isLocaleAlias(nodeText)) {
            state.extraStyle = {'text': styler.STYLE_EXPR_PREFIX + nodeText};
        } else {
            state.extraStyle = styler.createVariableStyle('text', "'" + U.trim(nodeText.replace(/'/g, "\\'")) + "'");
        }

        if (nodeText.match(/\{([^}]+)\}/) !== null) {
            state.extraStyle = nodeText;
        }
    }

    var nodeState = require('./default').parse(node, state);
    delete state.extraStyle;

    var code = nodeState.code;

    return {
        parent: {
            node: node,
            symbol: args.symbol
        },
        code: code
    };
}

alloy/Alloy/commands/compile/parsers/Ti.UI.Label

Ti.UI.LabelAttributedString が読み込まれるように以下のコードをコピペします。

var _ = require('../../../lib/alloy/underscore')._,
    styler = require('../styler'),
    CU = require('../compilerUtils'),
    U = require('../../../utils');

exports.parse = function(node, state) {
    return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
    var attributedStringsymbol,
        attributedStringObj = {},
        code = '';

    _.each(U.XML.getElementsFromNodes(node.childNodes), function(child){
        if (child.nodeName === 'AttributedString' && !child.hasAttribute('ns')) {
            child.setAttribute('ns', 'Ti.UI.iOS');
        }

        if (CU.validateNodeName(child, 'Ti.UI.iOS.AttributedString')) {
            code += CU.generateNodeExtended(child, state, {
                parent: {},
                post: function(node, state, args) {
                    attributedStringsymbol = state.parent.symbol;
                }
            });

            node.removeChild(child);
        }
    });

    if (attributedStringsymbol) {
        attributedStringObj = styler.createVariableStyle('attributedString', attributedStringsymbol);
    }

    // Get label text from node text, if present
    var nodeText = U.XML.getNodeText(node),
        textObj = {};
    if (nodeText) {
        if (U.isLocaleAlias(nodeText)) {
            textObj = {'text': styler.STYLE_EXPR_PREFIX + nodeText};
        } else {
            textObj = styler.createVariableStyle('text', "'" + U.trim(nodeText.replace(/'/g, "\\'")) + "'");
        }

        if (nodeText.match(/\{([^}]+)\}/) !== null) {
            textObj["text"] = nodeText;
        }
    }

    state.extraStyle = _.extend(attributedStringObj, textObj);

    var nodeState = require('./default').parse(node, state);
    code += nodeState.code;

    // Generate runtime code using default
    return _.extend(nodeState, {
        code: code
    });
}

alloy/Alloy/commands/compile/parsers/Ti.UI.TextField

Ti.UI.TextFieldAttributedStringAttributedHintText が読み込まれるように以下のコードをコピペします。

var _ = require('../../../lib/alloy/underscore')._,
    styler = require('../styler'),
    U = require('../../../utils'),
    CU = require('../compilerUtils'),
    CONST = require('../../../common/constants');

var KEYBOARD_TYPES = [
    'DEFAULT', 'ASCII', 'NUMBERS_PUNCTUATION', 'URL', 'EMAIL', 'DECIMAL_PAD', 'NAMEPHONE_PAD',
    'NUMBER_PAD', 'PHONE_PAD'
];
var RETURN_KEY_TYPES = [
    'DEFAULT', 'DONE', 'EMERGENCY_CALL', 'GO', 'GOOGLE', 'JOIN', 'NEXT', 'ROUTE',
    'SEARCH', 'SEND', 'YAHOO'
];
var AUTOCAPITALIZATION_TYPES = [
    'ALL', 'NONE', 'SENTENCES', 'WORDS'
];

exports.parse = function(node, state) {
    return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
    var code = '',
        postCode = '',
        extras = [],
        proxyProperties = {};

    // iterate through all children of the TextField
    _.each(U.XML.getElementsFromNodes(node.childNodes), function(child) {
        var fullname = CU.getNodeFullname(child),
            isProxyProperty = false,
            isControllerNode = false,
            isAttributedString = false,
            isAttributedHintText = false,
            hasUiNodes = false,
            controllerSymbol,
            parentSymbol;

        if (child.nodeName === 'AttributedString' && !child.hasAttribute('ns')) {
            child.setAttribute('ns', 'Ti.UI.iOS');
        }

        if (child.nodeName === 'AttributedHintText' && !child.hasAttribute('ns')) {
            child.nodeName = 'AttributedString';
            child.setAttribute('ns', 'Ti.UI.iOS');
            isAttributedHintText = true;
        }

        // validate the child element and determine if it's part of
        // the textfield or a proxy property assigment
        if (!CU.isNodeForCurrentPlatform(child)) {
            return;
        } else if (_.contains(CONST.CONTROLLER_NODES, fullname)) {
            isControllerNode = true;
        } else if (fullname.split('.')[0] === '_ProxyProperty') {
            isProxyProperty = true;
        } else if (CU.validateNodeName(child, 'Ti.UI.iOS.AttributedString')) {
            if (!isAttributedHintText) {
                isAttributedString = true;
            }
        }

        // generate the node
        code += CU.generateNodeExtended(child, state, {
            parent: {},
            post: function(node, state, args) {
                controllerSymbol = state.controller;
                parentSymbol = state.parent ? state.parent.symbol : state.item.symbol;
            }
        });

        // manually handle controller node proxy properties
        if (isControllerNode) {

            // set up any proxy properties at the top-level of the controller
            var inspect = CU.inspectRequireNode(child);
            _.each(_.uniq(inspect.names), function(name) {
                if (name.split('.')[0] === '_ProxyProperty') {
                    var prop = U.proxyPropertyNameFromFullname(name);
                    proxyProperties[prop] = controllerSymbol + '.getProxyPropertyEx("' + prop + '", {recurse:true})';
                } else {
                    hasUiNodes = true;
                }
            });
        }

        // generate code for proxy property assignments
        if (isProxyProperty) {
            proxyProperties[U.proxyPropertyNameFromFullname(fullname)] = parentSymbol;

        // generate code for the attribtuedString
        } else if (isAttributedString) {
            proxyProperties.attributedString = parentSymbol;
            node.removeChild(child);

        // generate code for the attribtuedHintText
        } else if (isAttributedHintText) {
            proxyProperties.attributedHintText = parentSymbol;
            node.removeChild(child);

        // generate code for the child components
        } else if (hasUiNodes || !isControllerNode) {
            postCode += '<%= parentSymbol %>.add(' + parentSymbol + ');';
        }

    });

    // support shortcuts for keyboard type, return key type, and autocapitalization
    var keyboardType = node.getAttribute('keyboardType');
    if (_.contains(KEYBOARD_TYPES, keyboardType.toUpperCase())) {
        node.setAttribute('keyboardType', 'Ti.UI.KEYBOARD_' + keyboardType.toUpperCase());
    }
    var returnKey = node.getAttribute('returnKeyType');
    if (_.contains(RETURN_KEY_TYPES, returnKey.toUpperCase())) {
        node.setAttribute('returnKeyType', 'Ti.UI.RETURNKEY_' + returnKey.toUpperCase());
    }
    var autocapitalization = node.getAttribute('keyboardType');
    if (_.contains(AUTOCAPITALIZATION_TYPES, autocapitalization.toUpperCase())) {
        node.setAttribute('autocapitalization', 'Ti.UI.TEXT_AUTOCAPITALIZATION_' + autocapitalization.toUpperCase());
    }


    // add all creation time properties to the state
    if (node.hasAttribute('clearOnEdit')) {
        var attr = node.getAttribute('clearOnEdit');
        extras.push(['clearOnEdit', attr === 'true']);
    }
    _.each(proxyProperties, function(v, k) {
        extras.push([k, v]);
    });
    if (extras.length) { state.extraStyle = styler.createVariableStyle(extras); }

    // generate the code for the textfield itself
    var nodeState = require('./default').parse(node, state);
    code += nodeState.code;

    // add the rows to the section
    if (nodeState.parent && postCode) {
        code += _.template(postCode, {
            parentSymbol: nodeState.parent.symbol
        });
    }

    // Update the parsing state
    return _.extend(state, {
        parent: nodeState.parent,
        code: code
    });
}

alloy/Alloy/commands/compile/parsers/Ti.UI.TextArea

Ti.UI.TextFieldAttributedString が読み込まれるように以下のコードをコピペします。

var _ = require('../../../lib/alloy/underscore')._,
    styler = require('../styler'),
    CU = require('../compilerUtils'),
    U = require('../../../utils'),
    CONST = require('../../../common/constants');

var KEYBOARD_TYPES = [
    'DEFAULT', 'ASCII', 'NUMBERS_PUNCTUATION', 'URL', 'EMAIL', 'DECIMAL_PAD', 'NAMEPHONE_PAD',
    'NUMBER_PAD', 'PHONE_PAD'
];
var RETURN_KEY_TYPES = [
    'DEFAULT', 'DONE', 'EMERGENCY_CALL', 'GO', 'GOOGLE', 'JOIN', 'NEXT', 'ROUTE',
    'SEARCH', 'SEND', 'YAHOO'
];
var AUTOCAPITALIZATION_TYPES = [
    'ALL', 'NONE', 'SENTENCES', 'WORDS'
];

exports.parse = function(node, state) {
    return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
    var code = '',
        postCode = '',
        extras = [],
        proxyProperties = {};

    // iterate through all children of the TextArea
    _.each(U.XML.getElementsFromNodes(node.childNodes), function(child) {
        var fullname = CU.getNodeFullname(child),
            isProxyProperty = false,
            isControllerNode = false,
            isAttributedString = false,
            hasUiNodes = false,
            controllerSymbol,
            parentSymbol;

        if (child.nodeName === 'AttributedString' && !child.hasAttribute('ns')) {
            child.setAttribute('ns', 'Ti.UI.iOS');
        }

        // validate the child element and determine if it's part of
        // the textarea or a proxy property assigment
        if (!CU.isNodeForCurrentPlatform(child)) {
            return;
        } else if (_.contains(CONST.CONTROLLER_NODES, fullname)) {
            isControllerNode = true;
        } else if (fullname.split('.')[0] === '_ProxyProperty') {
            isProxyProperty = true;
        } else if (CU.validateNodeName(child, 'Ti.UI.iOS.AttributedString')) {
            isAttributedString = true;
        }

        // generate the node
        code += CU.generateNodeExtended(child, state, {
            parent: {},
            post: function(node, state, args) {
                controllerSymbol = state.controller;
                parentSymbol = state.parent ? state.parent.symbol : null;
            }
        });

        // manually handle controller node proxy properties
        if (isControllerNode) {

            // set up any proxy properties at the top-level of the controller
            var inspect = CU.inspectRequireNode(child);
            _.each(_.uniq(inspect.names), function(name) {
                if (name.split('.')[0] === '_ProxyProperty') {
                    var prop = U.proxyPropertyNameFromFullname(name);
                    proxyProperties[prop] = controllerSymbol + '.getProxyPropertyEx("' + prop + '", {recurse:true})';
                } else {
                    hasUiNodes = true;
                }
            });
        }

        // generate code for proxy property assignments
        if (isProxyProperty) {
            proxyProperties[U.proxyPropertyNameFromFullname(fullname)] = parentSymbol;

        // generate code for the attribtuedString
        } else if (isAttributedString) {
            proxyProperties.attributedString = parentSymbol;
            node.removeChild(child);

        // generate code for the child components
        } else if (hasUiNodes || !isControllerNode) {
            postCode += '<%= parentSymbol %>.add(' + parentSymbol + ');';
        }

    });

    // support shortcuts for keyboard type, return key type, and autocapitalization
    var keyboardType = node.getAttribute('keyboardType');
    if (_.contains(KEYBOARD_TYPES, keyboardType.toUpperCase())) {
        node.setAttribute('keyboardType', 'Ti.UI.KEYBOARD_' + keyboardType.toUpperCase());
    }
    var returnKey = node.getAttribute('returnKeyType');
    if (_.contains(RETURN_KEY_TYPES, returnKey.toUpperCase())) {
        node.setAttribute('returnKeyType', 'Ti.UI.RETURNKEY_' + returnKey.toUpperCase());
    }
    var autocapitalization = node.getAttribute('keyboardType');
    if (_.contains(AUTOCAPITALIZATION_TYPES, autocapitalization.toUpperCase())) {
        node.setAttribute('autocapitalization', 'Ti.UI.TEXT_AUTOCAPITALIZATION_' + autocapitalization.toUpperCase());
    }


    // add all creation time properties to the state
    if (node.hasAttribute('clearOnEdit')) {
        var attr = node.getAttribute('clearOnEdit');
        extras.push(['clearOnEdit', attr === 'true']);
    }
    _.each(proxyProperties, function(v, k) {
        extras.push([k, v]);
    });
    if (extras.length) { state.extraStyle = styler.createVariableStyle(extras); }

    // generate the code for the textarea itself
    var nodeState = require('./default').parse(node, state);
    code += nodeState.code;

    // add the rows to the section
    if (nodeState.parent && postCode) {
        code += _.template(postCode, {
            parentSymbol: nodeState.parent.symbol
        });
    }

    // Update the parsing state
    return _.extend(state, {
        parent: nodeState.parent,
        code: code
    });
}

以上になります。特に、Ti.UI.TextField で hintText の色を変えたいとか、結構需要があったりしますよね。伝統芸能な、上にラベルをおいて change イベントを拾って。。。という実装もあるかと思いますが、attributedHintText を使ったほうが断然楽です。

注意事項

これは Alloy のコンパイラ自体に手を入れるため、自己責任でお願いします。ぼくの環境では問題なくコンパイルされますが、最悪 Alloy が壊れる可能性もあります。もちろん、Alloy 1.6 系で AttributedString がサポートされればこの改造は不要になります。