Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 53994038 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Associate native MeasuredText with Java one."

parents 8a8fe12d 6f1868b8
Loading
Loading
Loading
Loading
+125 −32
Original line number Diff line number Diff line
@@ -29,6 +29,10 @@ import android.text.style.MetricAffectingSpan;
import android.text.style.ReplacementSpan;
import android.util.Pools.SynchronizedPool;

import dalvik.annotation.optimization.CriticalNative;

import libcore.util.NativeAllocationRegistry;

import java.util.Arrays;

/**
@@ -57,6 +61,9 @@ import java.util.Arrays;
public class MeasuredText {
    private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';

    private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
            MeasuredText.class.getClassLoader(), nGetReleaseFunc(), 1024);

    private MeasuredText() {}  // Use build static functions instead.

    private static final SynchronizedPool<MeasuredText> sPool = new SynchronizedPool<>(1);
@@ -119,6 +126,26 @@ public class MeasuredText {
    // See getFontMetrics comments.
    private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);

    // The native MeasuredText.
    // See getNativePtr comments.
    // Do not modify these members directly. Use bindNativeObject/unbindNativeObject instead.
    private /* Maybe Zero */ long mNativePtr = 0;
    private @Nullable Runnable mNativeObjectCleaner;

    // Associate the native object to this Java object.
    private void bindNativeObject(/* Non Zero*/ long nativePtr) {
        mNativePtr = nativePtr;
        mNativeObjectCleaner = sRegistry.registerNativeAllocation(this, nativePtr);
    }

    // Decouple the native object from this Java object and release the native object.
    private void unbindNativeObject() {
        if (mNativePtr != 0) {
            mNativeObjectCleaner.run();
            mNativePtr = 0;
        }
    }

    // Following two objects are for avoiding object allocation.
    private @NonNull TextPaint mCachedPaint = new TextPaint();
    private @Nullable Paint.FontMetricsInt mCachedFm;
@@ -145,6 +172,7 @@ public class MeasuredText {
        mWidths.clear();
        mFontMetrics.clear();
        mSpanEndCache.clear();
        unbindNativeObject();
    }

    /**
@@ -225,6 +253,16 @@ public class MeasuredText {
        return mFontMetrics;
    }

    /**
     * Returns the native ptr of the MeasuredText.
     *
     * This is available only if the MeasureText is computed with computeForStaticLayout.
     * Returns 0 in other cases.
     */
    public /* Maybe Zero */ long getNativePtr() {
        return mNativePtr;
    }

    /**
     * Generates new MeasuredText for Bidi computation.
     *
@@ -308,7 +346,6 @@ public class MeasuredText {
     * @param start the inclusive start offset of the target region in the text
     * @param end the exclusive end offset of the target region in the text
     * @param textDir the text direction
     * @param nativeStaticLayoutPtr the pointer to the native static layout object
     * @param recycle pass existing MeasuredText if you want to recycle it.
     *
     * @return measured text
@@ -319,32 +356,47 @@ public class MeasuredText {
            @IntRange(from = 0) int start,
            @IntRange(from = 0) int end,
            @NonNull TextDirectionHeuristic textDir,
            /* Non-Zero */ long nativeStaticLayoutPtr,
            @Nullable MeasuredText recycle) {
        final MeasuredText mt = recycle == null ? obtain() : recycle;
        mt.resetAndAnalyzeBidi(text, start, end, textDir);
        if (mt.mTextLength == 0) {
            // Need to build empty native measured text for StaticLayout.
            // TODO: Stop creating empty measured text for empty lines.
            long nativeBuilderPtr = nInitBuilder();
            try {
                mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer));
            } finally {
                nFreeBuilder(nativeBuilderPtr);
            }
            return mt;
        }

        long nativeBuilderPtr = nInitBuilder();
        try {
            if (mt.mSpanned == null) {
                // No style change by MetricsAffectingSpan. Just measure all text.
            mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end,
                    nativeStaticLayoutPtr);
                mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr);
                mt.mSpanEndCache.append(end);
            } else {
            // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
                // There may be a MetricsAffectingSpan. Split into span transitions and apply
                // styles.
                int spanEnd;
                for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
                spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
                    spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
                                                             MetricAffectingSpan.class);
                    MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
                            MetricAffectingSpan.class);
                spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
                    spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
                                                       MetricAffectingSpan.class);
                    mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
                        nativeStaticLayoutPtr);
                                                 nativeBuilderPtr);
                    mt.mSpanEndCache.append(spanEnd);
                }
            }
            mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer));
        } finally {
            nFreeBuilder(nativeBuilderPtr);
        }

        return mt;
    }
@@ -415,13 +467,13 @@ public class MeasuredText {
    private void applyReplacementRun(@NonNull ReplacementSpan replacement,
                                     @IntRange(from = 0) int start,  // inclusive, in copied buffer
                                     @IntRange(from = 0) int end,  // exclusive, in copied buffer
                                     /* Maybe Zero */ long nativeStaticLayoutPtr) {
                                     /* Maybe Zero */ long nativeBuilderPtr) {
        // Use original text. Shouldn't matter.
        // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
        //       backward compatibility? or Should we initialize them for getFontMetricsInt?
        final float width = replacement.getSize(
                mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
        if (nativeStaticLayoutPtr == 0) {
        if (nativeBuilderPtr == 0) {
            // Assigns all width to the first character. This is the same behavior as minikin.
            mWidths.set(start, width);
            if (end > start + 1) {
@@ -429,25 +481,26 @@ public class MeasuredText {
            }
            mWholeWidth += width;
        } else {
            StaticLayout.addReplacementRun(nativeStaticLayoutPtr, mCachedPaint, start, end, width);
            nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
                               width);
        }
    }

    private void applyStyleRun(@IntRange(from = 0) int start,  // inclusive, in copied buffer
                               @IntRange(from = 0) int end,  // exclusive, in copied buffer
                               /* Maybe Zero */ long nativeStaticLayoutPtr) {
        if (nativeStaticLayoutPtr != 0) {
                               /* Maybe Zero */ long nativeBuilderPtr) {
        if (nativeBuilderPtr != 0) {
            mCachedPaint.getFontMetricsInt(mCachedFm);
        }

        if (mLtrWithoutBidi) {
            // If the whole text is LTR direction, just apply whole region.
            if (nativeStaticLayoutPtr == 0) {
            if (nativeBuilderPtr == 0) {
                mWholeWidth += mCachedPaint.getTextRunAdvances(
                        mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
                        mWidths.getRawArray(), start);
            } else {
                StaticLayout.addStyleRun(nativeStaticLayoutPtr, mCachedPaint, start, end,
                nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
                        false /* isRtl */);
            }
        } else {
@@ -458,14 +511,14 @@ public class MeasuredText {
            for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
                if (levelEnd == end || mLevels.get(levelEnd) != level) {  // transition point
                    final boolean isRtl = (level & 0x1) != 0;
                    if (nativeStaticLayoutPtr == 0) {
                    if (nativeBuilderPtr == 0) {
                        final int levelLength = levelEnd - levelStart;
                        mWholeWidth += mCachedPaint.getTextRunAdvances(
                                mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
                                isRtl, mWidths.getRawArray(), levelStart);
                    } else {
                        StaticLayout.addStyleRun(
                                nativeStaticLayoutPtr, mCachedPaint, levelStart, levelEnd, isRtl);
                        nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart,
                                levelEnd, isRtl);
                    }
                    if (levelEnd == end) {
                        break;
@@ -482,12 +535,12 @@ public class MeasuredText {
            @Nullable MetricAffectingSpan[] spans,
            @IntRange(from = 0) int start,  // inclusive, in original text buffer
            @IntRange(from = 0) int end,  // exclusive, in original text buffer
            /* Maybe Zero */ long nativeStaticLayoutPtr) {
            /* Maybe Zero */ long nativeBuilderPtr) {
        mCachedPaint.set(paint);
        // XXX paint should not have a baseline shift, but...
        mCachedPaint.baselineShift = 0;

        final boolean needFontMetrics = nativeStaticLayoutPtr != 0;
        final boolean needFontMetrics = nativeBuilderPtr != 0;

        if (needFontMetrics && mCachedFm == null) {
            mCachedFm = new Paint.FontMetricsInt();
@@ -512,9 +565,9 @@ public class MeasuredText {

        if (replacement != null) {
            applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
                    nativeStaticLayoutPtr);
                                nativeBuilderPtr);
        } else {
            applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeStaticLayoutPtr);
            applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
        }

        if (needFontMetrics) {
@@ -580,4 +633,44 @@ public class MeasuredText {
        }
        return width;
    }

    private static native /* Non Zero */ long nInitBuilder();

    /**
     * Apply style to make native measured text.
     *
     * @param nativeBuilderPtr The native MeasuredText builder pointer.
     * @param paintPtr The native paint pointer to be applied.
     * @param start The start offset in the copied buffer.
     * @param end The end offset in the copied buffer.
     * @param isRtl True if the text is RTL.
     */
    private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
                                            /* Non Zero */ long paintPtr,
                                            @IntRange(from = 0) int start,
                                            @IntRange(from = 0) int end,
                                            boolean isRtl);

    /**
     * Apply ReplacementRun to make native measured text.
     *
     * @param nativeBuilderPtr The native MeasuredText builder pointer.
     * @param paintPtr The native paint pointer to be applied.
     * @param start The start offset in the copied buffer.
     * @param end The end offset in the copied buffer.
     * @param width The width of the replacement.
     */
    private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
                                                  /* Non Zero */ long paintPtr,
                                                  @IntRange(from = 0) int start,
                                                  @IntRange(from = 0) int end,
                                                  @FloatRange(from = 0) float width);

    private static native long nBuildNativeMeasuredText(/* Non Zero */ long nativeBuilderPtr,
                                                 @NonNull char[] text);

    private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);

    @CriticalNative
    private static native /* Non Zero */ long nGetReleaseFunc();
}
+24 −47
Original line number Diff line number Diff line
@@ -48,6 +48,18 @@ import java.util.Arrays;
 * Canvas.drawText()} directly.</p>
 */
public class StaticLayout extends Layout {
    /*
     * The break iteration is done in native code. The protocol for using the native code is as
     * follows.
     *
     * First, call nInit to setup native line breaker object. Then, for each paragraph, do the
     * following:
     *
     *   - Create MeasuredText by MeasuredText.buildForStaticLayout which measures in native.
     *   - Run nComputeLineBreaks() to obtain line breaks for the paragraph.
     *
     * After all paragraphs, call finish() to release expensive buffers.
     */

    static final String TAG = "StaticLayout";

@@ -724,10 +736,11 @@ public class StaticLayout extends Layout {
                }

                measured = MeasuredText.buildForStaticLayout(
                        paint, source, paraStart, paraEnd, textDir, nativePtr, measured);
                        paint, source, paraStart, paraEnd, textDir, measured);
                final char[] chs = measured.getChars();
                final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
                final int[] fmCache = measured.getFontMetrics().getRawArray();
                // TODO: Stop keeping duplicated width copy in native and Java.
                widths.resize(chs.length);

                // measurement has to be done before performing line breaking
@@ -739,6 +752,7 @@ public class StaticLayout extends Layout {

                        // Inputs
                        chs,
                        measured.getNativePtr(),
                        paraEnd - paraStart,
                        firstWidth,
                        firstWidthLineCount,
@@ -873,18 +887,14 @@ public class StaticLayout extends Layout {

            if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
                    && mLineCount < mMaximumVisibleLineCount) {
                measured = MeasuredText.buildForStaticLayout(
                        paint, source, bufEnd, bufEnd, textDir, nativePtr, measured);

                paint.getFontMetricsInt(fm);

                v = out(source,
                        bufEnd, bufEnd, fm.ascent, fm.descent,
                        fm.top, fm.bottom,
                        v,
                        spacingmult, spacingadd, null,
                        null, fm, 0,
                        needMultiply, measured, bufEnd,
                        needMultiply, null, bufEnd,
                        includepad, trackpad, addLastLineSpacing, null,
                        null, bufStart, ellipsize,
                        ellipsizedWidth, 0, paint, false);
@@ -902,7 +912,7 @@ public class StaticLayout extends Layout {
    private int out(final CharSequence text, final int start, final int end, int above, int below,
            int top, int bottom, int v, final float spacingmult, final float spacingadd,
            final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
            final int flags, final boolean needMultiply, final MeasuredText measured,
            final int flags, final boolean needMultiply, @Nullable final MeasuredText measured,
            final int bufEnd, final boolean includePad, final boolean trackPad,
            final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
            final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
@@ -911,7 +921,7 @@ public class StaticLayout extends Layout {
        final int off = j * mColumns;
        final int want = off + mColumns + TOP;
        int[] lines = mLines;
        final int dir = measured.getParagraphDir();
        final int dir = (start == end) ? Layout.DIR_LEFT_TO_RIGHT : measured.getParagraphDir();

        if (want >= lines.length) {
            final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
@@ -938,7 +948,11 @@ public class StaticLayout extends Layout {
        lines[off + TAB] |= flags & TAB_MASK;
        lines[off + HYPHEN] = flags;
        lines[off + DIR] |= dir << DIR_SHIFT;
        if (start == end) {
            mLineDirections[j] = Layout.DIRS_ALL_LEFT_TO_RIGHT;
        } else {
            mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
        }

        final boolean firstLine = (j == 0);
        final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
@@ -1415,33 +1429,6 @@ public class StaticLayout extends Layout {
                mMaxLineHeight : super.getHeight();
    }

    /**
     * Measurement and break iteration is done in native code. The protocol for using
     * the native code is as follows.
     *
     * First, call nInit to setup native line breaker object. Then, for each paragraph, do the
     * following:
     *
     *   - Call one of the following methods for each run within the paragraph depending on the type
     *     of run:
     *     + addStyleRun (a text run, to be measured in native code)
     *     + addReplacementRun (a replacement run, width is given)
     *
     *   - Run nComputeLineBreaks() to obtain line breaks for the paragraph.
     *
     * After all paragraphs, call finish() to release expensive buffers.
     */

    /* package */ static void addStyleRun(long nativePtr, TextPaint paint, int start, int end,
            boolean isRtl) {
        nAddStyleRun(nativePtr, paint.getNativeInstance(), start, end, isRtl);
    }

    /* package */ static void addReplacementRun(long nativePtr, TextPaint paint, int start, int end,
            float width) {
        nAddReplacementRun(nativePtr, paint.getNativeInstance(), start, end, width);
    }

    @FastNative
    private static native long nInit(
            @BreakStrategy int breakStrategy,
@@ -1454,17 +1441,6 @@ public class StaticLayout extends Layout {
    @CriticalNative
    private static native void nFinish(long nativePtr);

    @CriticalNative
    private static native void nAddStyleRun(
            /* non-zero */ long nativePtr, /* non-zero */ long nativePaint,
            @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean isRtl);

    @CriticalNative
    private static native void nAddReplacementRun(
            /* non-zero */ long nativePtr, /* non-zero */ long nativePaint,
            @IntRange(from = 0) int start, @IntRange(from = 0) int end,
            @FloatRange(from = 0.0f) float width);

    // populates LineBreaks and returns the number of breaks found
    //
    // the arrays inside the LineBreaks objects are passed in as well
@@ -1477,6 +1453,7 @@ public class StaticLayout extends Layout {

            // Inputs
            @NonNull char[] text,
            /* Non Zero */ long measuredTextPtr,
            @IntRange(from = 0) int length,
            @FloatRange(from = 0.0f) float firstWidth,
            @IntRange(from = 0) int firstWidthLineCount,
+1 −0
Original line number Diff line number Diff line
@@ -84,6 +84,7 @@ cc_library_shared {
        "android_view_VelocityTracker.cpp",
        "android_text_AndroidCharacter.cpp",
        "android_text_Hyphenator.cpp",
        "android_text_MeasuredText.cpp",
        "android_text_StaticLayout.cpp",
        "android_os_Debug.cpp",
        "android_os_GraphicsEnvironment.cpp",
+2 −0
Original line number Diff line number Diff line
@@ -177,6 +177,7 @@ extern int register_android_net_NetworkUtils(JNIEnv* env);
extern int register_android_net_TrafficStats(JNIEnv* env);
extern int register_android_text_AndroidCharacter(JNIEnv *env);
extern int register_android_text_Hyphenator(JNIEnv *env);
extern int register_android_text_MeasuredText(JNIEnv* env);
extern int register_android_text_StaticLayout(JNIEnv *env);
extern int register_android_opengl_classes(JNIEnv *env);
extern int register_android_ddm_DdmHandleNativeHeap(JNIEnv *env);
@@ -1333,6 +1334,7 @@ static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_android_content_XmlBlock),
    REG_JNI(register_android_text_AndroidCharacter),
    REG_JNI(register_android_text_Hyphenator),
    REG_JNI(register_android_text_MeasuredText),
    REG_JNI(register_android_text_StaticLayout),
    REG_JNI(register_android_view_InputDevice),
    REG_JNI(register_android_view_KeyCharacterMap),
+122 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define LOG_TAG "MeasuredText"

#include "ScopedIcuLocale.h"
#include "unicode/locid.h"
#include "unicode/brkiter.h"
#include "utils/misc.h"
#include "utils/Log.h"
#include <nativehelper/ScopedStringChars.h>
#include <nativehelper/ScopedPrimitiveArray.h>
#include <nativehelper/JNIHelp.h>
#include "core_jni_helpers.h"
#include <cstdint>
#include <vector>
#include <list>
#include <algorithm>

#include "SkPaint.h"
#include "SkTypeface.h"
#include <hwui/MinikinSkia.h>
#include <hwui/MinikinUtils.h>
#include <hwui/Paint.h>
#include <minikin/FontCollection.h>
#include <minikin/AndroidLineBreakerHelper.h>
#include <minikin/MinikinFont.h>

namespace android {

static inline minikin::MeasuredTextBuilder* toBuilder(jlong ptr) {
    return reinterpret_cast<minikin::MeasuredTextBuilder*>(ptr);
}

static inline Paint* toPaint(jlong ptr) {
    return reinterpret_cast<Paint*>(ptr);
}

static inline minikin::MeasuredText* toMeasuredText(jlong ptr) {
    return reinterpret_cast<minikin::MeasuredText*>(ptr);
}

template<typename Ptr> static inline jlong toJLong(Ptr ptr) {
    return reinterpret_cast<jlong>(ptr);
}

static void releaseMeasuredText(jlong measuredTextPtr) {
    delete toMeasuredText(measuredTextPtr);
}

// Regular JNI
static jlong nInitBuilder() {
    return toJLong(new minikin::MeasuredTextBuilder());
}

// Regular JNI
static void nAddStyleRun(JNIEnv* /* unused */, jclass /* unused */, jlong builderPtr,
                         jlong paintPtr, jint start, jint end, jboolean isRtl) {
    Paint* paint = toPaint(paintPtr);
    const Typeface* typeface = Typeface::resolveDefault(paint->getAndroidTypeface());
    minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface);
    toBuilder(builderPtr)->addStyleRun(start, end, std::move(minikinPaint),
                                       typeface->fFontCollection, isRtl);
}

// Regular JNI
static void nAddReplacementRun(JNIEnv* /* unused */, jclass /* unused */, jlong builderPtr,
                               jlong paintPtr, jint start, jint end, jfloat width) {
    toBuilder(builderPtr)->addReplacementRun(start, end, width,
                                             toPaint(paintPtr)->getMinikinLocaleListId());
}

// Regular JNI
static jlong nBuildNativeMeasuredText(JNIEnv* env, jclass /* unused */, jlong builderPtr,
                                      jcharArray javaText) {
    ScopedCharArrayRO text(env, javaText);
    const minikin::U16StringPiece textBuffer(text.get(), text.size());

    // Pass the ownership to Java.
    return toJLong(toBuilder(builderPtr)->build(textBuffer).release());
}

// Regular JNI
static void nFreeBuilder(JNIEnv* env, jclass /* unused */, jlong builderPtr) {
    delete toBuilder(builderPtr);
}

// CriticalNative
static jlong nGetReleaseFunc() {
    return toJLong(&releaseMeasuredText);
}

static const JNINativeMethod gMethods[] = {
    // MeasuredTextBuilder native functions.
    {"nInitBuilder", "()J", (void*) nInitBuilder},
    {"nAddStyleRun", "(JJIIZ)V", (void*) nAddStyleRun},
    {"nAddReplacementRun", "(JJIIF)V", (void*) nAddReplacementRun},
    {"nBuildNativeMeasuredText", "(J[C)J", (void*) nBuildNativeMeasuredText},
    {"nFreeBuilder", "(J)V", (void*) nFreeBuilder},

    // MeasuredText native functions.
    {"nGetReleaseFunc", "()J", (void*) nGetReleaseFunc},  // Critical Natives
};

int register_android_text_MeasuredText(JNIEnv* env) {
    return RegisterMethodsOrDie(env, "android/text/MeasuredText", gMethods, NELEM(gMethods));
}

}
Loading