てっくらふと

IT技術系ブログ。たまにV系もあるかも : JetpackCompose / Django / FastAPI

ViewConfigurationの動作の解説と検証【JetpackCompose】

JetpackComposeには、UIのタップやスクロール動作のタイムアウト、サイズに関するパラメーターを指定するためのViewConfigurationというインターフェースが存在します。
CompositionLocalProviderで、このインターフェースを実装したクラスを渡すことにより、ロングタップまでの時間などを変更することが可能になります。

この記事ではViewConfigurationのそれぞれのパラメータが、実際のUI動作にどのような影響を及ぼすかを解説し検証します。

ViewConfigurationの作り方と渡し方

JetpackComposeには、値のバケツリレーを抑制するためにCompositionLocalという仕組みが存在します。
CompositionLocalを用いてViewConfigurationを渡すことにり、渡されたブロックより内側の動作が変更されます。

自分でViewConfigurationを実装したクラスを作成するには、一からViewConfigurationを実装したクラスを定義するか、今渡されているViewConfigurationを継承するかの2つの方法があります。
基本的には今渡されているViewConfigurationを継承するのが一般的で楽なので、そちらの方法を採用します。

例えば、長押しのタイムアウト時間を1000msに設定したViewConfigurationを作成して渡す方法は次のコードのようになります。

@Composable
fun ViewConfigurationProvideSample() {
    val defaultViewConfiguration = LocalViewConfiguration.current

    class SampleViewConfiguration : ViewConfiguration by defaultViewConfiguration {
        override val longPressTimeoutMillis: Long
            get() = 1000L
    }

    val sampleViewConfiguration = remember {
        SampleViewConfiguration()
    }

    CompositionLocalProvider(
        LocalViewConfiguration provides sampleViewConfiguration
    ) {
        // このブロック内では、長押しのタイムアウト時間が1000msに設定される
    }
}

longPressTimeoutMillis

longPressTimeoutMillisは、長押し(ロングタップ、ロングプレス)として判定されるまでの時間をミリ秒で指定します。

サンプルアプリ

サンプルアプリでは、longPressTimeoutMillisを3000に設定しています。
3000msタップし続けると、長押し判定となっていることがわかります。


クリックしてソースコードを表示

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LongPressTimeoutMillsSurvey() {
    // region: タイマー
    val coroutineScope = rememberCoroutineScope()
    var timerStartTime by remember {
        mutableLongStateOf(System.nanoTime())
    }
    var timerCurrentTime by remember {
        mutableLongStateOf(0L)
    }
    LaunchedEffect(Unit) {
        coroutineScope.launch(Dispatchers.Default) {
            while (true) {
                timerCurrentTime = (System.nanoTime() - timerStartTime) / 1000000L
                delay(10)
            }
        }
    }
    // endregion

    // region: 長押しまでの時間を3秒に設定するViewConfiguration
    val defaultViewConfiguration = LocalViewConfiguration.current

    class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration {
        override val longPressTimeoutMillis: Long
            get() = 3000L // 長押しまでの時間を3秒に設定
    }

    val appViewConfiguration = remember {
        AppViewConfiguration()
    }
    // endregion

    // region: 長押しが検出されたときのダイアログ
    var showLongPressDetectedDialog by remember {
        mutableStateOf(false)
    }
    if (showLongPressDetectedDialog) {
        AlertDialog(
            onDismissRequest = { showLongPressDetectedDialog = false },
            confirmButton = {
                Button(onClick = { showLongPressDetectedDialog = false }) {
                    Text(text = "OK")
                }
            },
            title = {
                Text(text = "Long press detected")
            },
        )
    }
    // endregion

    // region: UI本体
    CompositionLocalProvider(
        LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
        ) {
            Text(text = "Long press timer: $timerCurrentTime")
            Text(
                text = "Long press timeout: ${LocalViewConfiguration.current.longPressTimeoutMillis}ms",
                modifier = Modifier
                    .background(Color(0xFFFF6666))
                    .combinedClickable(
                        onClick = { },
                        onLongClick = { showLongPressDetectedDialog = true },
                    )
                    .pointerInput(Unit) {
                        awaitEachGesture {
                            awaitFirstDown(requireUnconsumed = true)
                            timerStartTime = System.nanoTime()
                        }
                    }
                    .padding(vertical = 16.dp)
            )
        }
    }
    // endregion
}



doubleTapTimeoutMillis

doubleTapTimeoutMillisは、2回目のタップがダブルタップとして判定されるまでのタイムアウト時間を指定します。

サンプルアプリ

サンプルアプリでは、doubleTapTimeoutMillisを2000に設定しています。
1回目のタップから、2000ms以内のタップがあればダブルタップ、2000ms以降では普通のタップとして認識されていることがわかります。


クリックしてソースコードを表示

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DoubleTapTimeoutMillsSurvey() {
    // region: タイマー
    val coroutineScope = rememberCoroutineScope()
    var timerStartTime by remember {
        mutableLongStateOf(System.nanoTime())
    }
    var timerCurrentTime by remember {
        mutableLongStateOf(0L)
    }
    LaunchedEffect(Unit) {
        coroutineScope.launch(Dispatchers.Default) {
            while (true) {
                timerCurrentTime = (System.nanoTime() - timerStartTime) / 1000000L
                delay(10)
            }
        }
    }
    // endregion

    // region: ダブルタップのタイムアウト時間を2秒に設定するViewConfiguration
    val defaultViewConfiguration = LocalViewConfiguration.current

    class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration {
        override val doubleTapTimeoutMillis: Long
            get() = 2000L // ダブルタップのタイムアウト時間を2秒に設定
    }

    val appViewConfiguration = remember {
        AppViewConfiguration()
    }
    // endregion

    // region: ダブルタップが検出されたときのダイアログ
    var showDoubleTapDetectedDialog by remember {
        mutableStateOf(false)
    }
    if (showDoubleTapDetectedDialog) {
        AlertDialog(
            onDismissRequest = { showDoubleTapDetectedDialog = false },
            confirmButton = {
                Button(onClick = { showDoubleTapDetectedDialog = false }) {
                    Text(text = "OK")
                }
            },
            title = {
                Text(text = "Long press detected")
            },
        )
    }
    // endregion

    // region: UI本体
    CompositionLocalProvider(
        LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
        ) {
            Text(text = "Timer: $timerCurrentTime")
            Text(
                text = "Double tap timeout: ${LocalViewConfiguration.current.doubleTapTimeoutMillis}ms",
                modifier = Modifier
                    .background(Color(0xFFFF6666))
                    .combinedClickable(
                        onClick = { },
                        onDoubleClick = { showDoubleTapDetectedDialog = true }
                    )
                    .pointerInput(Unit) {
                        awaitEachGesture {
                            awaitFirstDown(requireUnconsumed = true)
                            timerStartTime = System.nanoTime()
                        }
                    }
                    .padding(vertical = 16.dp)
            )
        }
    }
    // endregion
}


doubleTapMinTimeMillis

doubleTapMinTimeMillisは、ダブルタップとして扱われるまでの最小時間を指定します。

サンプルアプリ

サンプルアプリではdoubleTapMinTimeMillisを1000に設定しています。また、doubleTapTimeoutMillisは2000に設定しています。
1回目のタップから1000ms以下のタップはダブルタップとして認識されていません。
しかし、2回目以降(!)のタップが1000ms以降であればダブルタップとして認識されていることがわかります。

ここで注意するべきポイントは、2回目だけでなく2回目以降のタップでもダブルタップとして認識される点です。


クリックしてソースコードを表示

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun DoubleTapMinTimeMillsSurvey() {
    // region: タイマー
    val coroutineScope = rememberCoroutineScope()
    var timerStartTime by remember {
        mutableLongStateOf(System.nanoTime())
    }
    var timerCurrentTime by remember {
        mutableLongStateOf(0L)
    }
    var clickHistoryTime = remember {
        mutableStateListOf<Long>()
    }
    LaunchedEffect(Unit) {
        coroutineScope.launch(Dispatchers.Default) {
            while (true) {
                timerCurrentTime = (System.nanoTime() - timerStartTime) / 1000000L
                delay(10)
            }
        }
    }
    // endregion

    // region: ダブルタップの最小時間を2秒に設定するViewConfiguration
    val defaultViewConfiguration = LocalViewConfiguration.current

    class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration {
        override val doubleTapMinTimeMillis: Long
            get() = 1000L // ダブルタップの最小時間を1秒に設定
        override val doubleTapTimeoutMillis: Long
            get() = 2000L // ダブルタップのタイムアウト時間を2秒に設定
    }

    val appViewConfiguration = remember {
        AppViewConfiguration()
    }
    // endregion

    // region: ダブルタップが検出されたときのダイアログ
    var showDoubleTapDetectedDialog by remember {
        mutableStateOf(false)
    }
    if (showDoubleTapDetectedDialog) {
        AlertDialog(
            onDismissRequest = { showDoubleTapDetectedDialog = false },
            confirmButton = {
                Button(onClick = { showDoubleTapDetectedDialog = false }) {
                    Text(text = "OK")
                }
            },
            title = {
                Text(text = "Double tap detected")
            },
        )
    }
    // endregion

    // region: UI本体
    CompositionLocalProvider(
        LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
        ) {
            clickHistoryTime.forEachIndexed { index, historyTime ->
                val diff =
                    if (index == 0) "-" else (historyTime - clickHistoryTime[index - 1]).toString()
                Text(text = "Click $index: $historyTime ($diff)")
            }
            Text(text = "Timer: $timerCurrentTime")
            Text(
                text = """
                    Double tap timeout: ${LocalViewConfiguration.current.doubleTapTimeoutMillis}ms
                    Double tap min time: ${LocalViewConfiguration.current.doubleTapMinTimeMillis}ms
                """.trimIndent(),
                modifier = Modifier
                    .background(Color(0xFFFF6666))
                    .combinedClickable(
                        onClick = { },
                        onDoubleClick = { showDoubleTapDetectedDialog = true }
                    )
                    .pointerInput(Unit) {
                        awaitEachGesture {
                            awaitFirstDown(requireUnconsumed = true)
                            clickHistoryTime.add(timerCurrentTime)
                        }
                    }
                    .padding(vertical = 16.dp)
            )
        }
    }
    // endregion
}


touchSlop

touchSlopは、スクロールまたはドラッグとして扱われるまでのスワイプの距離をピクセル単位で指定します。

サンプルアプリ

サンプルアプリではtouchSlopを128dp分のピクセル数として指定しています。
touchSlop未満のスクロールではスクロールとして扱われず、touchSlop以上ではスクロールとして扱われます。

また、Modifier.pointerInputのdetectDragGesturesが発生するスワイプのしきい値にもこの値が使用されます。


クリックしてソースコードを表示

@Composable
private fun TouchSlopSurvey() {
    // region: TouchSlopを128に設定するViewConfiguration
    val defaultViewConfiguration = LocalViewConfiguration.current

    val density = LocalDensity.current
    val touchSlop = 128.dp
    val touchSlopPx = with(density) { touchSlop.toPx() }

    class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration {
        override val touchSlop: Float
            get() = touchSlopPx // TouchSlopを128に設定
    }

    val appViewConfiguration = remember {
        AppViewConfiguration()
    }
    // endregion

    val items = remember {
        (0..100).toList()
    }

    CompositionLocalProvider(
        LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定
    ) {
        Row(modifier = Modifier.fillMaxSize()) {
            Box(
                modifier = Modifier
                    .background(Color.Red)
                    .height(touchSlop)
                    .width(8.dp)
            )

            LazyColumn {
                items(items) { item ->
                    Text(
                        text = "Item $item",
                        modifier = Modifier
                            .background(if (item % 2 == 0) Color.Gray else Color.LightGray)
                            .padding(16.dp)
                            .fillMaxWidth()
                            .weight(1f, fill = false),
                    )
                }
            }
        }
    }
}


minimumTouchTargetSize

minimumTouchTargetSizeは、このサイズより小さい要素があったとき、タップ範囲が自動的にこのサイズまで拡大されます。

サンプルアプリ

サンプルアプリではminimumTouchTargetSizeを128dpに指定しています。
赤枠が128dp、緑枠が48dpとなっていて、赤枠をタップすればタップ判定が発生していることがわかります。


クリックしてソースコードを表示

@Composable
private fun MinimumTouchTargetSizeSurvey() {
    val context = LocalContext.current
    val defaultViewConfiguration = LocalViewConfiguration.current

    // region: MinimumTouchTargetSizeを128dpに設定するViewConfiguration
    class MinimumTouchTargetSize128ViewConfiguration :
        ViewConfiguration by defaultViewConfiguration {
        override val minimumTouchTargetSize: DpSize
            get() = DpSize(128.dp, 128.dp) // MinimumTouchTargetSizeを128dpに設定
    }

    val minimumTouchTargetSize128ViewConfiguration = remember {
        MinimumTouchTargetSize128ViewConfiguration()
    }
    // endregion

    CompositionLocalProvider(
        LocalViewConfiguration provides minimumTouchTargetSize128ViewConfiguration // ViewConfigurationを設定
    ) {
        Row(
            modifier = Modifier.fillMaxSize(),
            horizontalArrangement = Arrangement.SpaceEvenly,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier
                    .size(128.dp)
                    .background(Color.Red),
                contentAlignment = Alignment.Center,
            ) {
                Box(
                    modifier = Modifier
                        .size(48.dp)
                        .background(Color.Green)
                        .clickable {
                            Toast
                                .makeText(context, "Clicked", Toast.LENGTH_SHORT)
                                .show()
                        }
                )
            }
        }
    }
}


maximumFlingVelocity

maximumFlingVelocityは、スクロールのフライング動作(スクロールに速度がついて勝手に一定距離スクロールする動作)の最大速度をピクセル単位で指定します。

サンプルアプリ

サンプルアプリではmaximumFlingVelocityを600dp分のピクセル数として指定しています。
スクロールのフライング動作が落ちていることがわかります。


クリックしてソースコードを表示

@Composable
private fun MaximumFlingVelocitySurvey() {
    // region: MaximumFlingVelocityを600dp/sに設定するViewConfiguration
    val defaultViewConfiguration = LocalViewConfiguration.current

    val density = LocalDensity.current
    val maximumFlingVelocity = 600.dp
    val maximumFlingVelocityPx = with(density) { maximumFlingVelocity.toPx() }

    class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration {
        override val maximumFlingVelocity: Float
            get() = maximumFlingVelocityPx // MaximumFlingVelocityを600dp/sに設定
    }

    val appViewConfiguration = remember {
        AppViewConfiguration()
    }
    // endregion

    val items = remember {
        (0..100).toList()
    }

    CompositionLocalProvider(
        LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定
    ) {
        Row(modifier = Modifier.fillMaxSize()) {
            Box(
                modifier = Modifier
                    .background(Color.Red)
                    .height(maximumFlingVelocity)
                    .width(8.dp)
            )

            LazyColumn {
                items(items) { item ->
                    Text(
                        text = "Item $item",
                        modifier = Modifier
                            .background(if (item % 2 == 0) Color.Gray else Color.LightGray)
                            .padding(16.dp)
                            .fillMaxWidth()
                            .weight(1f, fill = false),
                    )
                }
            }
        }
    }
}


DjangoのValidationErrorのエラーメッセージ一覧を出力したい時、[error.message for error in ValidationError.error_list]を使うとハマるかもしれない話

こんにちは。うぃんちゃんです。
DjangoでValidationErrorのエラーメッセージを出力したい時に「ValidationError.error_list」を使うと、エラーメッセージがフォーマットされずハマる可能性があります。

結論

error変数をValidationError型とすると、

[inner_error_message for inner_error_message in error]

でOK。

どのような時にハマるか

[error.message for error in ValidationError.error_list]

のように書いた時、エラーメッセージがフォーマットされずに出力されてしまいます。

具体例を挙げると、Djangoのパスワードバリデーションが正しいかどうか次のviewを書いたとします。

@api_view(['POST'])
def validate_password(request):
    if 'password' not in request.data:
        return Response(status=400)

    try:
        password_validation.validate_password(request.data['password'])
        return JsonResponse({
            'result': True
        })
    except ValidationError as error:
        return JsonResponse({
            'result': False,
            'error_messages': [error.message for error in error.error_list]
        })

すると、最低文字数のエラーメッセージがフォーマットされずに次のように表示されてしまいます。

このパスワードは短すぎます。最低 %(min_length)d 文字以上必要です。

原因

本来、ValidationErrorをループで回すと__iter__関数により次の処理が走って正しい文字列にフォーマットされてくれます。

    def __iter__(self):
        if hasattr(self, 'error_dict'):
            for field, errors in self.error_dict.items():
                yield field, list(ValidationError(errors))
        else:
            for error in self.error_list:
                message = error.message
                if error.params:
                    message %= error.params # ここでフォーマットされてくれる
                yield str(message)

しかし、error_listからerrorを直接持ってきてmessageを取り出してしまうとフォーマットされません。

対策

冒頭に書いたとおり、

[inner_error for inner_error in error]

でOKです。
errorをループで回すと勝手に文字列にフォーマットされてエラー一覧の文字列の配列になります。

Vue.jsで条件によって動的にコンポーネントを変更[Vue.js+TypeScript]

お久しぶりです。

皆さんはVue.jsを使用していて、条件によってVue.jsのコンポーネントを変更したいということはありませんか?
if文で分けていく方法もありますが、コンポーネントや条件が多くなっていくとどんどん読みにくくなっていってしまいます。


そういった際に、どのような方法でプログラムを組めばいいかという一例を紹介します。

実装はVue.jsとTypeScriptを使ってやっていきます。

例として使用するプログラムの構成とイメージ

f:id:wintermaples:20200514075423j:plain

状況

データ(投票数とか、売上とか)の集計結果を表示したいが、データによってグラフを変えたり表示方法を変更したいなどの理由で、データの種類毎にコンポーネントを変更したい。

ソースコード

Factories.ts

import { VueConstructor } from "vue";

export abstract class Factory {
  /**
   * このFactoryクラスが、「引数に指定されたデータを 表示するためのコンポーネント」に変換するためのFactoryである場合はtrueを返します。
   * @param data データ
   */
  abstract isFactoryFor(data: Data): boolean;

  /**
   * 渡されたデータを表示するためのコンポーネントを生成します。
   * @param data 
   */
  abstract generateComponent(data: Data): VueConstructor<Vue>;

  static addFactory(factory: Factory): void {
    this.factories.push(factory);
  }

  static findFactory(data: Data): Factory|undefined {
    return this.factories.find(f => f.isFactoryFor(data));
  }

  static factories: Factory[] = [];
}


class PieChartFactory extends Factory {
  isFactoryFor(data: Data): boolean {
    //データの種類がPieChartを表示したい時にtrue
  }
  generateComponent(data: Data): VueConstructor<Vue> {
    //コンポーネントを生成
    //例:
    //return PieChartComponent;
  }
}

class LineChartFactory extends Factory {
  isFactoryFor(data: Data): boolean {
    //データの種類がLineChartを表示したい時にtrue
  }
  generateComponent(data: Data): VueConstructor<Vue> {
    //コンポーネントを生成
    //例:
    //return LineChartComponent;
  }
}

class TextAnswerFactory extends Factory {
  isFactoryFor(data: Data): boolean {
    //データの種類がTextAnswerを表示したい時にtrue
  }
  generateComponent(): VueConstructor<Vue> {
    //コンポーネントを生成
    //例:
    //return TextAnswerComponent;
  }
}

Factory.addFactory(new PieChartFactory());
Factory.addFactory(new LineChartFactory());
Factory.addFactory(new TextAnswerFactory());

Graphs.vue

<template>
<div>
  <component v-for="data in datas"
    :is="toChartComponent(data)"
    :data="data"
  />
</div>
</template>

<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";
import { Factory } from "Factories";

@Component
export default class App extends Vue {
  datas: Data[]|null = []; //データの配列。何らかの方法でデータを入れる
  toChartComponent(data): VueConstructor<Vue> {
    return Factory.findFactory(data).generateComponent();
  }
}
</script>

<style lang="scss" scoped>
</style>

※実際に使用する時は、各自で必要なところを変更してください。

プログラムの解説

原理としては、Factoryパターンを使用して実装しています。

まず、データの種類毎に、Factoryクラスを作っていってそれを登録(Factory.addFactory)していきます。
次に、toChartComponent(Graphs.vue内)でFactory.findFactoryを呼んであげて、データの種類によって使用するFactoryを分けています。
そして、generateComponentを呼んであげてコンポーネントを生成しています。

そして、vueのtemplate内にcomponentタグを作成してあげて「:is」でtoChartComponentを呼ぶことによって生成するコンポーネントを動的に変更することができます。

デモ


See the Pen
Blog-How-To-Change-Vuejs-Component-Dynamically
by wintermaples (@wintermaples)
on CodePen.


※グラフの表示は省略しています。

参考文献

コンポーネント - vue.js


間違っている点、こうしたらいいよ!って所があればコメントよろしくおねがいしますm(_ _)m

平成30年 春期 基本情報・応用情報 偏差値表(大体)

自分が基本情報に受かったときも作ったけど、今回応用情報を受けて合格しましたのでまた作ってみました。
正規分布に従うと過程すればもっと正確に近い偏差値が出せるんですが、めんどくさいのでだいぶはしょって計算しています。

応用情報 午前

点数 人数 偏差値
90~100 107 76.91509161
80~89 1474 69.37786546
70~79 5052 61.8406393
60~69 8194 54.30341314
50~59 8048 46.76618698
40~49 5330 39.22896083
30~39 1951 31.69173467
20~29 260 24.15450851
10~19 17 16.61728235
0~9 2 9.080056197

μ=59.29045507
σ=13.26748036

応用情報 午後

点数 人数 偏差値
90~100 21 79.92948455
80~89 386 71.68944914
70~79 2099 63.44941373
60~69 4411 55.20937832
50~59 4489 46.96934291
40~49 2474 38.7293075
30~39 698 30.48927209
20~29 136 22.24923668
10~19 32 14.00920127
0~9 4 5.769165863

μ=58.6779661
σ=12.13587018

基本情報 午前

点数 人数 偏差値
90~100 177 73.99590791
80~89 2823 66.97312243
70~79 9502 59.95033696
60~69 11848 52.92755148
50~59 9490 45.904766
40~49 6182 38.88198052
30~39 2936 31.85919504
20~29 757 24.83640957
10~19 39 17.81362409
0~9 6 10.79083861

μ=60.83135283
σ=14.23936418

基本情報 午後

点数 人数 偏差値
90~100 625 73.60067585
80~89 2605 68.1938841
70~79 5285 62.78709234
60~69 8108 57.38030059
50~59 9928 51.97350883
40~49 9744 46.56671708
30~39 7908 41.15992533
20~29 4588 35.75313357
10~19 1500 30.34634182
0~9 437 24.93955006

μ=51.3499448
σ=18.49525644


ただし上記は次のように計算した。
ある得点範囲Aの下限をkとする。(例えば、30~39点ならk=30)
Aの範囲内の得点を取った人数をn(A)とするとき、k+5点を取った人がn(A)人であると仮定する。(例えば、基本情報午後の30~39点なら、n(30~39)=7908)
あとは正規分布に従うと仮定して、偏差値を通常通り導出する。


ちなみに自分の結果はこんな感じです。
f:id:wintermaples:20180622003049p:plain
午前偏差値: 66.55
午後偏差値: 67.57

基本情報よりなぜかかなり上がった(*´艸`*)

ItaEditor 1.1.2をリリースしました[ItaEditor][NetBeans][java][プログラミング]

修正遅れて申し訳ありません。m_m

バグ修正

背景画像タブがなくなることがあるのを修正

まだ完全にバグが解消されたかどうかを確認したわけではないのでまだバグが残ってる可能性があります。
何かあればコメントお願い致します。

ダウンロード先:
https://onedrive.live.com/redir?resid=600857656291DE8D!1010&authkey=!AH0SiLo5lIRlMaM&ithint=file%2cnbm

CSS Design created by satotaka99. Thankyou.