テキスト エディター アプリ

 2024年4月29日10時頃、ScrollViewとConstraintLayoutとListViewの組み合わせでは動作不良でテキスト ファイルの内容を全てスクロールして閲覧する事ができなかったので、LinearLayoutに変更する修正をしました。



 ちなみに、デフォルトではScrollView内のListViewは、ScrollViewがタッチ イベントをListViewに渡してくれないため、ListViewのスクロールが動作しないので、アクティビティなどでScrollViewに対してrequestDisallowInterceptTouchEvent(true)を実行してタッチ イベントを渡すように変更する必要が有ります。



 2024年4月24日22時頃、contentResolver.openOutputStream()で"wt"モードを指定していなかったため、書き込む予定のテキストよりも、書き込む前のファイルの内容のテキストの文字数のほうが大きい場合、書き込む前のファイルの内容のテキストの先頭の一部を置換するような形に成ってしまい、書き込む前のファイルの内容のテキストの末尾が切り捨てられずに残ってしまう不具合を修正しました。



 Githubでテキスト エディター アプリの.apkファイルなどをパブリック ドメインで公開しております。

 マイクロソフトのBing検索エンジンで「github android-simple-text-editor」などで検索してみてください。

 残念ながらグーグル検索エンジンでは検索できません。



 広告無しのテキスト エディター アプリを自作しました。


 文字コードを指定してテキスト ファイルを読み込めます。

 文字コードと改行コードを指定してテキスト ファイルを保存できます。

 戻す、アン ドゥ、Un-doとやり直す、リ ドゥ、Re-doできます。

 文字パターン「正規表現」で検索できます。

 文字パターン「正規表現」で置換、全置換できます。


 AQUOS sense3で動作を確認できました。



 ※下記のXMLファイルやKotlinのプログラムなどのコードをコピペする場合は、2文字の全角空白を4文字の半角空白に置換してください。


 また、Android StudioにJavaやKotlinなどのプログラムのコードをコピペして、「import android.R」が自動で追加されてしまったら、削除してください。

 「android.R」は、「R.layout.activity_main」や「R.id.◯◯◯」の「R」とは違います。

 そのため、「import android.R」が有ると、コンパイル エラーが発生してしまいます。


 Android StudioにJavaやKotlinなどのプログラムのコードをコピペすると、変数の名前が半角バッククォート記号(`)で囲まれる事が有ります。

 Kotlinでは変数の名前を半角バッククォート記号(`)で囲むと予約語(inやnullなど)や半角空白記号( )などを変数の名前にできるそうです。

 可能であれば、半角バッククォート記号(`)で囲まれた変数の名前は、半角バッククォート記号(`)で囲まずに済む名前に変更したほうが良いのでは、と個人的に思っております。



/home/◯◯◯/AndroidStudioProjects/SimpleTextEditor/app/src/main/AndroidManifest.xml

――――――――――――――――――――

<?xml version="1.0" encoding="utf-8"?>

<manifest

  xmlns:android="http://schemas.android.com/apk/res/android"

  xmlns:tools="http://schemas.android.com/tools"

>

  <application

    android:allowBackup="true"

    android:dataExtractionRules="@xml/data_extraction_rules"

    android:fullBackupContent="@xml/backup_rules"

    android:icon="@mipmap/ic_launcher"

    android:label="@string/app_name"

    android:supportsRtl="true"

    android:theme="@style/Theme.SimpleTextEditor"

    tools:targetApi="31"

  >


    <activity

      android:name=".MainActivity"

      android:exported="true"

    >

      <intent-filter>

        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />

      </intent-filter>

    </activity>


    <activity

      android:name=".Edit"

      android:parentActivityName=".MainActivity"

    >

      <meta-data

        android:name="android.support.PARENT_ACTIVITY"

        android:value=".MainActivity"

      />

    </activity>


  </application>

</manifest>

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 SimpleTextEditorは著者が付けたAndroid Studioのプロジェクトの名前です。



/home/◯◯◯/AndroidStudioProjects/SimpleTextEditor/app/src/main/res/values/strings.xml

――――――――――――――――――――

<resources>

  <string name="app_name">SimpleTextEditor</string>


  <string name="read">Read</string>

  <string name="edit">Edit</string>

  <string name="copy_all">Copy All</string>


  <string name="character_code_name_default">UTF-8</string>

  <string name="line_break_default">\\n</string>


  <string name="view">View</string>

  <string name="save">Save</string>

  <string name="undo">Undo</string>

  <string name="redo">Redo</string>

  <string name="search">Search</string>

  <string name="replace">Replace</string>

  <string name="replace_all">Replace All</string>


  <string name="editing_file_name_prefix">*</string>


  <string name="empty_search_pattern">The "Regular Expression" of search pattern is empty. Could you input?</string>

  <string name="wrong_search_pattern">The "Regular Expression" of search pattern is wrong. </string>

  <string name="search_result_zero">Search Result: Zero Record</string>

</resources>

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 SimpleTextEditorは著者が付けたAndroid Studioのプロジェクトの名前です。



/home/◯◯◯/AndroidStudioProjects/SimpleTextEditor/app/src/main/res/values-ja/strings.xml

――――――――――――――――――――

<resources>

  <string name="app_name">SimpleTextEditor</string>


  <string name="read">テキスト ファイル読み込み</string>

  <string name="edit">編集</string>

  <string name="copy_all">全コピー</string>


  <string name="character_code_name_default">UTF-8</string>

  <string name="line_break_default">\\n</string>


  <string name="view">ビュー</string>

  <string name="save">保存</string>

  <string name="undo">戻す</string>

  <string name="redo">やり直す</string>

  <string name="search">検索</string>

  <string name="replace">置換</string>

  <string name="replace_all">全置換</string>


  <string name="editing_file_name_prefix">※</string>


  <string name="empty_search_pattern">文字パターン「正規表現」を検索条件に入力してください。</string>

  <string name="wrong_search_pattern">検索条件の文字パターン「正規表現」が誤っているので直してください。</string>

  <string name="search_result_zero">検索結果: ゼロ件</string>

</resources>

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 SimpleTextEditorは著者が付けたAndroid Studioのプロジェクトの名前です。



/home/◯◯◯/AndroidStudioProjects/SimpleTextEditor/app/src/main/res/layout/activity_main.xml

――――――――――――――――――――

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:orientation="vertical"

  android:layout_width="match_parent"

  android:layout_height="match_parent"

>


  <TextView

    android:id="@+id/viewFileName"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:text=""

  />


  <EditText

    android:id="@+id/viewCharacterCodeName"

    android:inputType="text"

    android:layout_width="match_parent"

    android:layout_height="50dp"

    android:text="@string/character_code_name_default"

  />


  <EditText

    android:id="@+id/viewLineBreak"

    android:inputType="text"

    android:layout_width="match_parent"

    android:layout_height="50dp"

    android:text="@string/line_break_default"

  />


  <Button

    android:id="@+id/viewRead"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@string/read"

    android:layout_gravity="center"

  />


  <Button

    android:id="@+id/viewEdit"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@string/edit"

    android:layout_gravity="center"

  />


  <Button

    android:id="@+id/viewCopyAll"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@string/copy_all"

    android:layout_gravity="center"

  />


  <ListView

    android:id="@+id/viewLineList"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

  />


</LinearLayout>

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 SimpleTextEditorは著者が付けたAndroid Studioのプロジェクトの名前です。



/home/◯◯◯/AndroidStudioProjects/SimpleTextEditor/app/src/main/res/layout/view_line.xml

――――――――――――――――――――

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:orientation="vertical"

  android:layout_width="match_parent"

  android:layout_height="match_parent"

>


  <TextView

    android:id="@+id/viewLine"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:gravity="left"

    android:layout_gravity="left"

  />


</LinearLayout>

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 SimpleTextEditorは著者が付けたAndroid Studioのプロジェクトの名前です。



/home/◯◯◯/AndroidStudioProjects/SimpleTextEditor/app/src/main/res/layout/edit.xml

――――――――――――――――――――

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:orientation="vertical"

  android:layout_width="match_parent"

  android:layout_height="match_parent"

>

  <ScrollView

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:fillViewport="true"

  >


    <androidx.constraintlayout.widget.ConstraintLayout

      xmlns:android="http://schemas.android.com/apk/res/android"

      xmlns:app="http://schemas.android.com/apk/res-auto"

      android:layout_width="match_parent"

      android:layout_height="match_parent"

    >


      <TextView

        android:id="@+id/editFileName"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:text=""

        app:layout_constraintTop_toTopOf="parent"

        app:layout_constraintStart_toStartOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

      />


      <EditText

        android:id="@+id/editCharacterCodeName"

        android:inputType="text"

        android:layout_width="match_parent"

        android:layout_height="50dp"

        android:text=""

        app:layout_constraintTop_toBottomOf="@id/editFileName"

        app:layout_constraintStart_toStartOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

      />


      <EditText

        android:id="@+id/editLineBreak"

        android:inputType="text"

        android:layout_width="match_parent"

        android:layout_height="50dp"

        android:text=""

        app:layout_constraintTop_toBottomOf="@id/editCharacterCodeName"

        app:layout_constraintStart_toStartOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

      />


      <Button

        android:id="@+id/editView"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="@string/view"

        app:layout_constraintTop_toBottomOf="@id/editLineBreak"

        app:layout_constraintStart_toStartOf="parent"

      />


      <Button

        android:id="@+id/editSave"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="@string/save"

        app:layout_constraintTop_toBottomOf="@id/editLineBreak"

        app:layout_constraintStart_toEndOf="@id/editView"

        app:layout_constraintEnd_toEndOf="parent"

        app:layout_constraintHorizontal_bias="1.0"

      />


      <Button

        android:id="@+id/editUndo"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="@string/undo"

        app:layout_constraintTop_toBottomOf="@id/editView"

        app:layout_constraintStart_toStartOf="parent"

      />


      <Button

        android:id="@+id/editRedo"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="@string/redo"

        app:layout_constraintTop_toBottomOf="@id/editView"

        app:layout_constraintStart_toEndOf="@id/editUndo"

        app:layout_constraintEnd_toEndOf="parent"

        app:layout_constraintHorizontal_bias="1.0"

      />


      <EditText

        android:id="@+id/editSearchPattern"

        android:inputType="text"

        android:layout_width="match_parent"

        android:layout_height="50dp"

        android:text=""

        app:layout_constraintTop_toBottomOf="@id/editUndo"

        app:layout_constraintStart_toStartOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

      />


      <Button

        android:id="@+id/editSearch"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="@string/search"

        app:layout_constraintTop_toBottomOf="@id/editSearchPattern"

        app:layout_constraintStart_toStartOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

      />


      <EditText

        android:id="@+id/editReplaceText"

        android:inputType="text"

        android:layout_width="match_parent"

        android:layout_height="50dp"

        android:text=""

        app:layout_constraintTop_toBottomOf="@id/editSearch"

        app:layout_constraintStart_toStartOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

      />


      <Button

        android:id="@+id/editReplace"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="@string/replace"

        app:layout_constraintTop_toBottomOf="@id/editReplaceText"

        app:layout_constraintStart_toStartOf="parent"

      />


      <Button

        android:id="@+id/editReplaceAll"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="@string/replace_all"

        app:layout_constraintTop_toBottomOf="@id/editReplaceText"

        app:layout_constraintStart_toEndOf="@id/editReplace"

        app:layout_constraintEnd_toEndOf="parent"

        app:layout_constraintHorizontal_bias="1.0"

      />


      <EditText

        android:id="@+id/editText"

        android:inputType="textMultiLine"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        app:layout_constraintTop_toBottomOf="@id/editReplaceAll"

        app:layout_constraintStart_toStartOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

        app:layout_constraintBottom_toBottomOf="parent"

        app:layout_constraintHorizontal_bias="0.0"

        app:layout_constraintVertical_bias="0.0"

      >

        <requestFocus/>

      </EditText>


    </androidx.constraintlayout.widget.ConstraintLayout>

  </ScrollView>

</LinearLayout>

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 SimpleTextEditorは著者が付けたAndroid Studioのプロジェクトの名前です。



/home/◯◯◯/AndroidStudioProjects/SimpleTextEditor/app/src/main/java/eliphas1810/simpletexteditor/MainActivity.kt

――――――――――――――――――――

package eliphas1810.simpletexteditor


import android.content.ClipData

import android.content.ClipboardManager

import android.content.Context

import android.content.Intent

import android.net.Uri

import androidx.appcompat.app.AppCompatActivity

import android.os.Bundle

import android.view.LayoutInflater

import android.view.View

import android.view.ViewGroup

import android.widget.*

import androidx.activity.result.ActivityResult

import androidx.activity.result.ActivityResultLauncher

import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult

import androidx.documentfile.provider.DocumentFile

import java.io.InputStreamReader

import java.nio.charset.Charset



private class Adapter(context: Context, lineList: List<String>, ): ArrayAdapter<String>(context, R.layout.view_line) {


  var maxLineNumberSize = 1


  override fun getView(position: Int, view: View?, viewGroup: ViewGroup): View {

    var view: View? = view

    try {

      if (view == null) {

        view = LayoutInflater.from(context).inflate(R.layout.view_line, viewGroup, false)

      }


      var line = getItem(position)


      var lineNumber = "" + (position + 1)


      view?.findViewById<TextView>(R.id.viewLine)?.text = lineNumber + "  " + line


    } catch (exception: Exception) {

      Toast.makeText(view?.context?.applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    }

    return view!!

  }

}



class MainActivity : AppCompatActivity() {



  companion object {

    const val FILE_NAME_KEY = "eliphas1810.simpletexteditor.FILE_NAME"

    const val CHARACTER_CODE_KEY = "eliphas1810.simpletexteditor.CHARACTER_CODE"

    const val LINE_BREAK_KEY = "eliphas1810.simpletexteditor.LINE_BREAK"

    const val TEXT_KEY = "eliphas1810.simpletexteditor.TEXT"

    const val CURSOR_TEXT_INDEX_KEY = "eliphas1810.simpletexteditor.CURSOR_TEXT_INDEX"

  }



  var fileNameTextView: TextView? = null


  var characterCodeEditText: EditText? = null


  var lineBreakEditText: EditText? = null


  var viewLineListView: ListView? = null



  private var text: String? = ""



  private var readActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(StartActivityForResult()) { activityResult: ActivityResult ->


    if (activityResult.resultCode == RESULT_OK) {

      val intent = activityResult.data


      val uri: Uri? = intent?.data


      val documentFile = DocumentFile.fromSingleUri(applicationContext, uri!!)


      fileNameTextView?.text = documentFile?.name


      var characterCode = characterCodeEditText?.text?.toString()!!

      if (characterCode.isEmpty()) {

        characterCodeEditText?.setText(getString(R.string.character_code_name_default))

        characterCode = getString(R.string.character_code_name_default)

      }

      try {

        Charset.forName(characterCode)

      } catch (exception: Exception) {

        characterCodeEditText?.setText(getString(R.string.character_code_name_default))

        characterCode = getString(R.string.character_code_name_default)

      }


      InputStreamReader(contentResolver.openInputStream(uri!!), characterCode).use {

        text = it.readText()

      }


      if (text?.contains("\r\n") == true) {

        lineBreakEditText?.setText("\\r\\n")

      } else if (text?.contains("\r") == true) {

        lineBreakEditText?.setText("\\r")

      } else {

        lineBreakEditText?.setText("\\n")

      }


      text = text?.replace("\r\n", "\n")

      text = text?.replace("\r", "\n")

      var lineList: List<String> = text!!.split("\n")

      if (lineList.isEmpty()) {

       lineList = listOf("")

      }


      val adapter = viewLineListView?.adapter as Adapter

      adapter.clear()

      adapter.addAll(lineList)

      adapter.maxLineNumberSize = lineList.size

      adapter.notifyDataSetChanged()

    }

  }



  private var editActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(StartActivityForResult()) { activityResult: ActivityResult ->


    if (activityResult.resultCode == RESULT_OK) {

      val intent = activityResult.data


      fileNameTextView?.text = intent?.getStringExtra(FILE_NAME_KEY)


      characterCodeEditText?.setText(intent?.getStringExtra(CHARACTER_CODE_KEY))


      lineBreakEditText?.setText(intent?.getStringExtra(LINE_BREAK_KEY))


      text = intent?.getStringExtra(TEXT_KEY)


      text = text?.replace("\r\n", "\n")

      text = text?.replace("\r", "\n")

      var lineList: List<String> = text!!.split("\n")

      if (lineList.isEmpty()) {

        lineList = listOf("")

      }


      val adapter = viewLineListView?.adapter as Adapter

      adapter.clear()

      adapter.addAll(lineList)

      adapter.maxLineNumberSize = lineList.size

      adapter.notifyDataSetChanged()

    }

  }



  override fun onCreate(savedInstanceState: Bundle?) {

    try {

      super.onCreate(savedInstanceState)

      setContentView(R.layout.activity_main)



      fileNameTextView = findViewById(R.id.viewFileName)


      characterCodeEditText = findViewById(R.id.viewCharacterCodeName)


      lineBreakEditText = findViewById(R.id.viewLineBreak)



      viewLineListView = findViewById(R.id.viewLineList)


      val adapter = Adapter(this, listOf(""))

      adapter.maxLineNumberSize = 1

      viewLineListView?.adapter = adapter



      viewLineListView?.onItemClickListener = AdapterView.OnItemClickListener { adapterView, view, position, id ->

        try {


          text = text?.replace("\r\n", "\n")

          text = text?.replace("\r", "\n")

          var lineList: List<String> = text!!.split("\n")

          if (lineList.isEmpty()) {

            lineList = listOf("")

          }


          var cursorTextIndex = 0

          if (1 <= position) {

            cursorTextIndex = lineList.slice(0..(position - 1)).joinToString("\n").length + 1

          }


          val intent = Intent(this, Edit::class.java)

          intent.putExtra(FILE_NAME_KEY, fileNameTextView?.text)

          intent.putExtra(CHARACTER_CODE_KEY, characterCodeEditText?.text?.toString())

          intent.putExtra(LINE_BREAK_KEY, lineBreakEditText?.text?.toString())

          intent.putExtra(TEXT_KEY, text)

          intent.putExtra(CURSOR_TEXT_INDEX_KEY, cursorTextIndex)

          startActivity(intent)


        } catch (exception: Exception) {

          Toast.makeText(view.context.applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      findViewById<Button>(R.id.viewRead).setOnClickListener{ view ->

        try {


          val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)

          intent.addCategory(Intent.CATEGORY_OPENABLE)

          intent.type = "*/*"

          readActivityResultLauncher?.launch(intent)


        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      findViewById<Button>(R.id.viewEdit).setOnClickListener{ view ->

        try {


          val intent = Intent(this, Edit::class.java)

          intent.putExtra(FILE_NAME_KEY, fileNameTextView?.text)

          intent.putExtra(CHARACTER_CODE_KEY, characterCodeEditText?.text?.toString())

          intent.putExtra(LINE_BREAK_KEY, lineBreakEditText?.text?.toString())

          intent.putExtra(TEXT_KEY, text)

          intent.putExtra(CURSOR_TEXT_INDEX_KEY, text?.length!!)

          editActivityResultLauncher?.launch(intent)


        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      findViewById<Button>(R.id.viewCopyAll).setOnClickListener{ view ->

        try {


          val clipboardManager = applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager

          clipboardManager.setPrimaryClip(ClipData.newPlainText("", text))


        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    }

  }



  override fun onDestroy() {

    try {



      fileNameTextView = null


      characterCodeEditText = null


      lineBreakEditText = null



      viewLineListView = null



      text = null



      readActivityResultLauncher?.unregister()

      readActivityResultLauncher = null



      editActivityResultLauncher?.unregister()

      editActivityResultLauncher = null



    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    } finally {


      super.onDestroy()

    }

  }

}

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 SimpleTextEditorは著者が付けたAndroid Studioのプロジェクトの名前です。

 eliphas1810/simpletexteditorは著者が付けたJavaやKotlinのプログラムのパッケージのディレクトリの相対パスです。

 eliphas1810.simpletexteditorは著者が付けたJavaやKotlinのプログラムのパッケージの名前です。



/home/◯◯◯/AndroidStudioProjects/SimpleTextEditor/app/src/main/java/eliphas1810/simpletexteditor/Edit.kt

――――――――――――――――――――

package eliphas1810.simpletexteditor


import android.content.Intent

import android.net.Uri

import android.os.Bundle

import android.text.Editable

import android.text.TextWatcher

import android.widget.*

import androidx.activity.OnBackPressedCallback

import androidx.activity.result.ActivityResult

import androidx.activity.result.ActivityResultLauncher

import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult

import androidx.appcompat.app.AppCompatActivity

import androidx.documentfile.provider.DocumentFile

import java.io.BufferedWriter

import java.io.OutputStreamWriter

import java.nio.charset.Charset



//テキスト編集履歴

data class History (

  var text: String = "", //テキスト編集履歴を保持した時点のテキスト

  var cursorIndex: Int = 0 //テキスト編集履歴を保持した時点のテキスト内のカーソルのインデックス番号

) {

}



class Edit : AppCompatActivity(), TextWatcher {



  companion object {

    const val FILE_NAME_KEY = "eliphas1810.simpletexteditor.FILE_NAME"

    const val CHARACTER_CODE_KEY = "eliphas1810.simpletexteditor.CHARACTER_CODE"

    const val LINE_BREAK_KEY = "eliphas1810.simpletexteditor.LINE_BREAK"

    const val TEXT_KEY = "eliphas1810.simpletexteditor.TEXT"

    const val CURSOR_TEXT_INDEX_KEY = "eliphas1810.simpletexteditor.CURSOR_TEXT_INDEX"

  }



  var fileNameTextView: TextView? = null


  var characterCodeEditText: EditText? = null


  var lineBreakEditText: EditText? = null


  var editText: EditText? = null



  var fileName: String? = null

  var editingFileName: String? = null //テキスト編集中を表すためのファイル名



  private var historyList: MutableList<History>? = mutableListOf() //テキスト編集履歴の一覧

  private var historyIndex: Int? = -1



  private var saveActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(StartActivityForResult()) { activityResult: ActivityResult ->


    if (activityResult.resultCode == RESULT_OK) {

      val intent = activityResult.data


      val uri: Uri? = intent?.data


      val documentFile = DocumentFile.fromSingleUri(applicationContext, uri!!)


      fileName = documentFile?.name

      editingFileName = getString(R.string.editing_file_name_prefix) + fileName


      fileNameTextView?.text = fileName


      editText = findViewById<EditText>(R.id.editText)


      var text = editText?.text?.toString()


      var lineBreak = "\n"

      if (lineBreakEditText?.text?.toString() == "\\n") {

        lineBreak = "\n"

      } else if (lineBreakEditText?.text?.toString() == "\\r\\n") {

        lineBreak = "\r\n"

      } else if (lineBreakEditText?.text?.toString() == "\\r") {

        lineBreak = "\r"

      } else {

        lineBreak = "\n"

      }


      if (lineBreak == "\n") {

        lineBreakEditText?.setText("\\n")

      } else if (lineBreak == "\r\n") {

        lineBreakEditText?.setText("\\r\\n")

      } else if (lineBreak == "\r") {

        lineBreakEditText?.setText("\\r")

      }


      text = text?.replace("\r\n", "\n")

      text = text?.replace("\r", "\n")

      text = text?.replace("\n", lineBreak)


      var characterCode = characterCodeEditText?.text?.toString()!!

      if (characterCode.isEmpty()) {

        characterCodeEditText?.setText(getString(R.string.character_code_name_default))

        characterCode = getString(R.string.character_code_name_default)

      }

      try {

        Charset.forName(characterCode)

      } catch (exception: Exception) {

        characterCodeEditText?.setText(getString(R.string.character_code_name_default))

        characterCode = getString(R.string.character_code_name_default)

      }


      //contentResolver.openOutputStream()で"wt"モードを指定しないと、書き込み前のテキストの文字数が大きい場合、書き込み前のテキストの先頭の一部を置換するような形に成ってしまいます。

      OutputStreamWriter(contentResolver.openOutputStream(uri!!, "wt"), characterCode).use {

        BufferedWriter(it).use {

          it.write(text)

        }

      }

    }

  }



  override fun onCreate(savedInstanceState: Bundle?) {

    try {

      super.onCreate(savedInstanceState)

      setContentView(R.layout.edit)



      //戻るボタン、戻るジェスチャーを無効化

      onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {

        override fun handleOnBackPressed() {}

      })



      fileNameTextView = findViewById(R.id.editFileName)


      characterCodeEditText = findViewById(R.id.editCharacterCodeName)


      lineBreakEditText = findViewById(R.id.editLineBreak)


      editText = findViewById<EditText>(R.id.editText)


      fileName = getIntent()?.getStringExtra(FILE_NAME_KEY)

      editingFileName = getString(R.string.editing_file_name_prefix) + fileName


      fileNameTextView?.text = fileName


      characterCodeEditText?.setText(getIntent()?.getStringExtra(CHARACTER_CODE_KEY))


      lineBreakEditText?.setText(getIntent()?.getStringExtra(LINE_BREAK_KEY))


      editText?.setText(getIntent()?.getStringExtra(TEXT_KEY))

      editText?.setSelection(getIntent()?.getIntExtra(CURSOR_TEXT_INDEX_KEY, 0)!!)


      historyList?.clear()

      historyList?.add(History(editText?.text?.toString()!!, editText?.text?.toString()?.length!!))

      historyIndex = historyList?.size!! - 1


      findViewById<Button>(R.id.editUndo).isEnabled = false

      findViewById<Button>(R.id.editRedo).isEnabled = false



      findViewById<Button>(R.id.editView).setOnClickListener{ view ->

        try {


          historyList?.clear()

          historyIndex = -1


          findViewById<Button>(R.id.editUndo).isEnabled = false

          findViewById<Button>(R.id.editRedo).isEnabled = false


          val intent = Intent(this, MainActivity::class.java)

          intent.putExtra(FILE_NAME_KEY, fileName)

          intent.putExtra(CHARACTER_CODE_KEY, characterCodeEditText?.text?.toString())

          intent.putExtra(LINE_BREAK_KEY, lineBreakEditText?.text?.toString())

          intent.putExtra(TEXT_KEY, editText?.text.toString())

          setResult(RESULT_OK, intent)


          fileName = null

          editingFileName = null


          finish()


        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      findViewById<Button>(R.id.editSave).setOnClickListener{ view ->

        try {



          if (historyIndex!! <= -1) {

            return@setOnClickListener

          }


          while (historyIndex!! < historyList?.size!! - 1) {

            historyList?.removeLast()

          }


          historyList?.add(History(editText?.text?.toString()!!, editText?.selectionEnd ?: editText?.text?.toString()?.length!!))

          historyIndex = historyList?.size!! - 1


          findViewById<Button>(R.id.editUndo).isEnabled = true

          findViewById<Button>(R.id.editRedo).isEnabled = false


          val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)

          intent.addCategory(Intent.CATEGORY_OPENABLE)

          intent.type = "*/*"

          saveActivityResultLauncher?.launch(intent)


        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      findViewById<Button>(R.id.editUndo).setOnClickListener{ view ->

        try {



          if (historyIndex!! <= 0) {

            return@setOnClickListener

          }


          historyIndex = historyIndex!! - 1


          editText?.setText(historyList?.get(historyIndex!!)?.text!!)

          editText?.setSelection(historyList?.get(historyIndex!!)?.cursorIndex!!)


          findViewById<Button>(R.id.editRedo).isEnabled = true


          if (historyIndex == 0) {


            findViewById<Button>(R.id.editUndo).isEnabled = false


            fileNameTextView?.text = fileName


          } else {

            fileNameTextView?.text = editingFileName

          }



        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      findViewById<Button>(R.id.editRedo).setOnClickListener{ view ->

        try {



          if (historyIndex!! <= -1) {

            return@setOnClickListener

          }


          if (historyList?.size!! - 1 <= historyIndex!!) {

            return@setOnClickListener

          }


          historyIndex = historyIndex!! + 1


          editText?.setText(historyList?.get(historyIndex!!)?.text!!)

          editText?.setSelection(historyList?.get(historyIndex!!)?.cursorIndex!!)


          findViewById<Button>(R.id.editUndo).isEnabled = true


          fileNameTextView?.text = editingFileName


          if (historyList?.size!! - 1 <= historyIndex!!) {


            findViewById<Button>(R.id.editRedo).isEnabled = false

          }



        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      findViewById<Button>(R.id.editSearch).setOnClickListener{ view ->

        try {



          val searchPattern = findViewById<EditText>(R.id.editSearchPattern)?.text?.toString()!!


          if (searchPattern.isEmpty()) {

            Toast.makeText(applicationContext, getString(R.string.empty_search_pattern), Toast.LENGTH_LONG).show()

            return@setOnClickListener

          }


          var searchRegex: Regex? = null

          try {

            searchRegex = Regex(searchPattern)

          } catch (exception: Exception) {

            Toast.makeText(applicationContext, getString(R.string.wrong_search_pattern) + exception.toString(), Toast.LENGTH_LONG).show()

            throw exception

          }


          if (editText?.hasFocus() == false) {

            editText?.requestFocus()

            editText?.setSelection(0)

          }


          val text = editText?.text?.toString()!!

          val searchStartIndex = editText?.selectionEnd!!


          val matchResult = searchRegex?.find(text, searchStartIndex)


          if (matchResult == null) {

            Toast.makeText(applicationContext, getString(R.string.search_result_zero), Toast.LENGTH_LONG).show()

            return@setOnClickListener

          }


          editText?.setSelection(matchResult?.range?.start!!, matchResult?.range?.endInclusive!! + 1)



        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      findViewById<Button>(R.id.editReplace).setOnClickListener{ view ->

        try {



          val searchPattern = findViewById<EditText>(R.id.editSearchPattern)?.text?.toString()!!

          val replaceText = findViewById<EditText>(R.id.editReplaceText)?.text?.toString()!!


          if (searchPattern.isEmpty()) {

            Toast.makeText(applicationContext, getString(R.string.empty_search_pattern), Toast.LENGTH_LONG).show()

            return@setOnClickListener

          }


          var searchRegex: Regex? = null

          try {

            searchRegex = Regex(searchPattern)

          } catch (exception: Exception) {

            Toast.makeText(applicationContext, getString(R.string.wrong_search_pattern) + exception.toString(), Toast.LENGTH_LONG).show()

            throw exception

          }


          if (editText?.hasFocus() == false) {

            editText?.requestFocus()

            editText?.setSelection(0)

          }


          var text = editText?.text?.toString()!!

          val searchStartIndex = editText?.selectionStart!!


          val matchResult = searchRegex?.find(text, searchStartIndex)


          if (matchResult == null) {

            Toast.makeText(applicationContext, getString(R.string.search_result_zero), Toast.LENGTH_LONG).show()

            return@setOnClickListener

          }


          val startIndex = matchResult?.range?.start!!

          val endIndex = matchResult?.range?.endInclusive!!


          val replacedText = text.substring(startIndex, endIndex + 1).replace(Regex(searchPattern), replaceText)


          text = text.substring(0, startIndex) + replacedText + text.substring(endIndex + 1)


          while (historyIndex!! < historyList?.size!! - 1) {

            historyList?.removeLast()

          }


          historyList?.add(History(text, startIndex + replaceText.length))

          historyIndex = historyList?.size!! - 1


          editText?.setText(text)

          editText?.setSelection(startIndex + replaceText.length)


          fileNameTextView?.text = editingFileName


          findViewById<Button>(R.id.editUndo).isEnabled = true

          findViewById<Button>(R.id.editRedo).isEnabled = false



        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      findViewById<Button>(R.id.editReplaceAll).setOnClickListener{ view ->

        try {



          val searchPattern = findViewById<EditText>(R.id.editSearchPattern)?.text?.toString()!!

          val replaceText = findViewById<EditText>(R.id.editReplaceText)?.text?.toString()!!


          if (searchPattern.isEmpty()) {

            Toast.makeText(applicationContext, getString(R.string.empty_search_pattern), Toast.LENGTH_LONG).show()

            return@setOnClickListener

          }


          var searchRegex: Regex? = null

          try {

            searchRegex = Regex(searchPattern)

          } catch (exception: Exception) {

            Toast.makeText(applicationContext, getString(R.string.wrong_search_pattern) + exception.toString(), Toast.LENGTH_LONG).show()

            throw exception

          }


          var text = editText?.text?.toString()!!


          if (searchRegex?.find(text) == null) {

            Toast.makeText(applicationContext, getString(R.string.search_result_zero), Toast.LENGTH_LONG).show()

            return@setOnClickListener

          }


          text = text.replace(Regex(searchPattern), replaceText)


          while (historyIndex!! < historyList?.size!! - 1) {

            historyList?.removeLast()

          }


          var cursorIndex = editText?.selectionEnd ?: 0

          if (text?.length!! < cursorIndex) {

            cursorIndex = text?.length!!

          }

          historyList?.add(History(text, cursorIndex))

          historyIndex = historyList?.size!! - 1


          editText?.setText(text)

          editText?.setSelection(cursorIndex)


          fileNameTextView?.text = editingFileName


          findViewById<Button>(R.id.editUndo).isEnabled = true

          findViewById<Button>(R.id.editRedo).isEnabled = false


        } catch (exception: Exception) {

          Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

          throw exception

        }

      }



      editText?.addTextChangedListener(this)



    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    }

  }



  override fun onDestroy() {

    try {



      fileNameTextView = null


      characterCodeEditText = null


      lineBreakEditText = null


      editText = null



      fileName = null

      editingFileName = null



      historyList?.clear()

      historyList = null


      historyIndex = null



      saveActivityResultLauncher?.unregister()

      saveActivityResultLauncher = null



    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    } finally {


      super.onDestroy()

    }

  }



  //EditTextのtextが変更されている時に1度だけ、変更前のtextなどを渡されて起動されます。

  override fun beforeTextChanged(text: CharSequence?, index: Int, changedSize: Int, addedSize: Int) {

  }



  //EditTextのtextが変更されている時に変更のたびに、変更中のtextなどを渡されて起動されます。

  override fun onTextChanged(text: CharSequence?, index: Int, deletedSize: Int, addedSize: Int) {

  }



  //EditTextのtextが変更された時に、変更後のtextを渡されて起動されます。

  override fun afterTextChanged(text: Editable?) {

    try {


      if (historyIndex!! <= -1) {

        return

      }


      val lastText = historyList?.get(historyIndex!!)?.text!!


      if (lastText == text?.toString()) {

        return

      }


      while (historyIndex!! < historyList?.size!! - 1) {

        historyList?.removeLast()

      }


      historyList?.add(History(text?.toString() ?: "", editText?.selectionEnd ?: text?.toString()?.length ?: 0))

      historyIndex = historyList?.size!! - 1


      fileNameTextView?.text = editingFileName


      findViewById<Button>(R.id.editUndo).isEnabled = true

      findViewById<Button>(R.id.editRedo).isEnabled = false


    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    }

  }

}

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 SimpleTextEditorは著者が付けたAndroid Studioのプロジェクトの名前です。

 eliphas1810/simpletexteditorは著者が付けたJavaやKotlinのプログラムのパッケージのディレクトリの相対パスです。

 eliphas1810.simpletexteditorは著者が付けたJavaやKotlinのプログラムのパッケージの名前です。

  • Xで共有
  • Facebookで共有
  • はてなブックマークでブックマーク

作者を応援しよう!

ハートをクリックで、簡単に応援の気持ちを伝えられます。(ログインが必要です)

応援したユーザー

応援すると応援コメントも書けます

新規登録で充実の読書を

マイページ
読書の状況から作品を自動で分類して簡単に管理できる
小説の未読話数がひと目でわかり前回の続きから読める
フォローしたユーザーの活動を追える
通知
小説の更新や作者の新作の情報を受け取れる
閲覧履歴
以前読んだ小説が一覧で見つけやすい
新規ユーザー登録無料

アカウントをお持ちの方はログイン

カクヨムで可能な読書体験をくわしく知る