diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ff5a76747..f099547f5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -185,22 +185,22 @@ dependencies { //noinspection GradleDependency implementation("com.github.Cosmic-Ide:DependencyResolver:868996895a") implementation("com.google.android.material:material:1.12.0") - implementation("com.google.code.gson:gson:2.10.1") + implementation("com.google.code.gson:gson:2.11.0") implementation("com.github.haroldadmin:WhatTheStack:1.0.0-alpha04") implementation("androidx.appcompat:appcompat:1.7.0") - implementation("androidx.constraintlayout:constraintlayout:2.2.0-alpha14") + implementation("androidx.constraintlayout:constraintlayout:2.2.0-beta01") implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.core:core-splashscreen:1.1.0-rc01") implementation("androidx.documentfile:documentfile:1.1.0-alpha01") - implementation("androidx.fragment:fragment-ktx:1.8.2") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4") + implementation("androidx.fragment:fragment-ktx:1.8.3") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.5") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.viewpager2:viewpager2:1.1.0") - implementation("androidx.activity:activity-ktx:1.9.1") - implementation("androidx.startup:startup-runtime:1.2.0-alpha02") + implementation("androidx.activity:activity-ktx:1.9.2") + implementation("androidx.startup:startup-runtime:1.2.0-rc01") val editorVersion = "0.23.4-96c0abc-SNAPSHOT" //noinspection GradleDependency @@ -255,7 +255,7 @@ dependencies { implementation(projects.feature.treeView) // jgit uses some methods like `transferTo` are only available from Android 13 onwards - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.2") testImplementation("junit:junit:4.13.2") } diff --git a/app/src/main/assets/textmate/QuietLight.tmTheme.json b/app/src/main/assets/textmate/QuietLight.tmTheme.json index 022f8c9ce..0730b2b1d 100644 --- a/app/src/main/assets/textmate/QuietLight.tmTheme.json +++ b/app/src/main/assets/textmate/QuietLight.tmTheme.json @@ -62,7 +62,7 @@ "name": "Keywords", "scope": "keyword, storage", "settings": { - "foreground": "#4B83CD" + "foreground": "#4B8ECD" } }, { @@ -120,7 +120,7 @@ "name": "Numbers, Characters", "scope": "constant.numeric, constant.character, constant", "settings": { - "foreground": "#AB6526" + "foreground": "#888888" } }, { @@ -171,14 +171,14 @@ }, { "name": "HTML: Doctype Declaration", - "scope": "meta.tag.sgml.doctype, meta.tag.sgml.doctype string, meta.tag.sgml.doctype\n entity.name.tag, meta.tag.sgml punctuation.definition.tag.html\n ", + "scope": "meta.tag.sgml.doctype, meta.tag.sgml.doctype string, meta.tag.sgml.doctype entity.name.tag, meta.tag.sgml punctuation.definition.tag.html", "settings": { "foreground": "#AAAAAA" } }, { "name": "HTML: Tags", - "scope": "meta.tag, punctuation.definition.tag.html,\n punctuation.definition.tag.begin.html, punctuation.definition.tag.end.html\n ", + "scope": "meta.tag, punctuation.definition.tag.html, punctuation.definition.tag.begin.html, punctuation.definition.tag.end.html", "settings": { "foreground": "#91B3E0" } @@ -216,7 +216,7 @@ }, { "name": "HTML: Attribute Names", - "scope": "meta.tag entity.other.attribute-name, entity.other.attribute-name.html\n ", + "scope": "meta.tag entity.other.attribute-name, entity.other.attribute-name.html", "settings": { "foreground": "#91B3E0" } @@ -234,7 +234,7 @@ }, { "name": "CSS: Selectors", - "scope": "meta.selector, meta.selector entity, meta.selector entity punctuation,\n entity.name.tag.css\n ", + "scope": "meta.selector, meta.selector entity, meta.selector entity punctuation, entity.name.tag.css", "settings": { "foreground": "#7A3E9D" } @@ -248,7 +248,7 @@ }, { "name": "CSS: Property Values", - "scope": "meta.property-value, meta.property-value constant.other,\n support.constant.property-value\n ", + "scope": "meta.property-value, meta.property-value constant.other, support.constant.property-value", "settings": { "foreground": "#448C27" } @@ -409,6 +409,5 @@ "foreground": "#434343" } } - ], - "uuid": "231D6A91-5FD1-4CBE-BD2A-0F36C08693F1" -} \ No newline at end of file + ] +} diff --git a/app/src/main/kotlin/org/cosmicide/App.kt b/app/src/main/kotlin/org/cosmicide/App.kt index c7e93c69c..6a5309bdb 100644 --- a/app/src/main/kotlin/org/cosmicide/App.kt +++ b/app/src/main/kotlin/org/cosmicide/App.kt @@ -25,7 +25,6 @@ import de.robv.android.xposed.XC_MethodHook import io.github.rosemoe.sora.langs.textmate.registry.FileProviderRegistry import io.github.rosemoe.sora.langs.textmate.registry.GrammarRegistry import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry -import io.github.rosemoe.sora.langs.textmate.registry.model.ThemeModel import io.github.rosemoe.sora.langs.textmate.registry.provider.AssetsFileResolver import org.cosmicide.common.Analytics import org.cosmicide.common.Prefs @@ -35,7 +34,6 @@ import org.cosmicide.rewrite.plugin.api.HookManager import org.cosmicide.rewrite.plugin.api.PluginLoader import org.cosmicide.rewrite.util.FileUtil import org.cosmicide.util.CommonUtils -import org.eclipse.tm4e.core.registry.IThemeSource import org.lsposed.hiddenapibypass.HiddenApiBypass import rikka.sui.Sui import java.io.File @@ -67,7 +65,6 @@ class App : Application() { if (FileUtil.isInitialized.not()) return - Log.d("Analytics", "Initializing") Analytics.init(this@App) Log.d("Analytics", "Sending event") @@ -217,12 +214,6 @@ class App : Application() { FileProviderRegistry.getInstance().addFileProvider(fileProvider) GrammarRegistry.getInstance().loadGrammars("textmate/languages.json") - - val themeRegistry = ThemeRegistry.getInstance() - themeRegistry.loadTheme(loadTheme("darcula.json", "darcula")) - themeRegistry.loadTheme(loadTheme("QuietLight.tmTheme.json", "QuietLight")) - - applyThemeBasedOnConfiguration() } private fun setupHooks() { @@ -334,12 +325,4 @@ class App : Application() { PluginLoader.loadPlugin(dir, plugin) } } - - fun loadTheme(fileName: String, themeName: String): ThemeModel { - val inputStream = - FileProviderRegistry.getInstance().tryGetInputStream("textmate/$fileName") - ?: throw FileNotFoundException("Theme file not found: $fileName") - val source = IThemeSource.fromInputStream(inputStream, fileName, null) - return ThemeModel(source, themeName) - } } diff --git a/app/src/main/kotlin/org/cosmicide/MainActivity.kt b/app/src/main/kotlin/org/cosmicide/MainActivity.kt index 7292554fe..0d26efe52 100644 --- a/app/src/main/kotlin/org/cosmicide/MainActivity.kt +++ b/app/src/main/kotlin/org/cosmicide/MainActivity.kt @@ -20,15 +20,19 @@ import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import com.google.android.material.color.DynamicColors +import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry +import io.github.rosemoe.sora.langs.textmate.registry.model.ThemeModel import kotlinx.coroutines.launch import org.cosmicide.common.Prefs import org.cosmicide.databinding.ActivityMainBinding import org.cosmicide.fragment.InstallResourcesFragment import org.cosmicide.fragment.ProjectFragment import org.cosmicide.util.CommonUtils +import org.cosmicide.util.MaterialEditorTheme import org.cosmicide.util.ResourceUtil import org.cosmicide.util.awaitBinderReceived import org.cosmicide.util.isShizukuInstalled +import org.eclipse.tm4e.core.registry.IThemeSource import rikka.shizuku.Shizuku import rikka.shizuku.Shizuku.OnRequestPermissionResultListener import rikka.shizuku.ShizukuProvider @@ -64,6 +68,7 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) enableEdgeToEdge() + loadEditorThemes() ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> val imeInset = @@ -119,7 +124,7 @@ class MainActivity : AppCompatActivity() { } } - fun requestPermission() { + private fun requestPermission() { if (Shizuku.isPreV11()) { requestPermissions(arrayOf(ShizukuProvider.PERMISSION), shizukuPermissionCode) } else { @@ -127,6 +132,22 @@ class MainActivity : AppCompatActivity() { } } + private fun loadEditorThemes() { + val themeRegistry = ThemeRegistry.getInstance() + themeRegistry.loadTheme(loadTheme("darcula.json", "darcula")) + themeRegistry.loadTheme(loadTheme("QuietLight.tmTheme.json", "QuietLight")) + + App.instance.get()!!.applyThemeBasedOnConfiguration() + } + + + private fun loadTheme(fileName: String, themeName: String): ThemeModel { + val inputStream = + MaterialEditorTheme.resolveTheme(this, fileName) + val source = IThemeSource.fromInputStream(inputStream, fileName, null) + return ThemeModel(source, themeName) + } + override fun onDestroy() { super.onDestroy() Shizuku.removeRequestPermissionResultListener(listener) diff --git a/app/src/main/kotlin/org/cosmicide/editor/IdeEditor.kt b/app/src/main/kotlin/org/cosmicide/editor/IdeEditor.kt index f676b5891..30b6e316f 100644 --- a/app/src/main/kotlin/org/cosmicide/editor/IdeEditor.kt +++ b/app/src/main/kotlin/org/cosmicide/editor/IdeEditor.kt @@ -11,12 +11,12 @@ import android.content.Context import android.content.res.Configuration import android.util.AttributeSet import android.view.inputmethod.EditorInfo +import com.google.android.material.color.MaterialColors import com.google.common.collect.ImmutableSet import io.github.rosemoe.sora.langs.textmate.TextMateColorScheme import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry import io.github.rosemoe.sora.widget.CodeEditor import io.github.rosemoe.sora.widget.component.EditorDiagnosticTooltipWindow -import io.github.rosemoe.sora.widget.schemes.EditorColorScheme import org.cosmicide.common.Prefs import org.cosmicide.editor.language.TsLanguageJava import org.cosmicide.extension.setCompletionLayout @@ -102,7 +102,13 @@ class IdeEditor @JvmOverloads constructor( private fun setTooltipImprovements() { getComponent(EditorDiagnosticTooltipWindow::class.java).apply { setSize(500, 100) - parentView.setBackgroundColor(colorScheme.getColor(EditorColorScheme.WHOLE_BACKGROUND)) + parentView.setBackgroundColor( + MaterialColors.getColor( + context, + com.google.android.material.R.attr.colorErrorContainer, + null + ) + ) } } diff --git a/app/src/main/kotlin/org/cosmicide/editor/language/Util.kt b/app/src/main/kotlin/org/cosmicide/editor/language/TreeSitterUtil.kt similarity index 99% rename from app/src/main/kotlin/org/cosmicide/editor/language/Util.kt rename to app/src/main/kotlin/org/cosmicide/editor/language/TreeSitterUtil.kt index f72b7200f..9a603e376 100644 --- a/app/src/main/kotlin/org/cosmicide/editor/language/Util.kt +++ b/app/src/main/kotlin/org/cosmicide/editor/language/TreeSitterUtil.kt @@ -16,7 +16,7 @@ import io.github.rosemoe.sora.editor.ts.predicate.builtin.MatchPredicate import io.github.rosemoe.sora.lang.styling.TextStyle import io.github.rosemoe.sora.widget.schemes.EditorColorScheme -object Util { +object TreeSitterUtil { fun applyTheme(desc: TsThemeBuilder) { desc.apply { TextStyle.makeStyle( @@ -95,4 +95,4 @@ object Util { ) ) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/cosmicide/editor/language/TsLanguageJava.kt b/app/src/main/kotlin/org/cosmicide/editor/language/TsLanguageJava.kt index c032eceaa..e0ac47517 100644 --- a/app/src/main/kotlin/org/cosmicide/editor/language/TsLanguageJava.kt +++ b/app/src/main/kotlin/org/cosmicide/editor/language/TsLanguageJava.kt @@ -172,12 +172,12 @@ class TsLanguageJava( project: Project, file: File ) = TsLanguageJava( - Util.createLanguageSpec( + TreeSitterUtil.createLanguageSpec( TS_LANGUAGE_JAVA, editor.context.assets, "java" ), - { Util.applyTheme(it) }, + { TreeSitterUtil.applyTheme(it) }, editor, project, file diff --git a/app/src/main/kotlin/org/cosmicide/extension/context.kt b/app/src/main/kotlin/org/cosmicide/extension/context.kt index 6a8dfe71b..a4e8bdb43 100644 --- a/app/src/main/kotlin/org/cosmicide/extension/context.kt +++ b/app/src/main/kotlin/org/cosmicide/extension/context.kt @@ -12,6 +12,9 @@ import android.content.ClipboardManager import android.content.Context import android.util.TypedValue import androidx.core.content.ContextCompat +import com.google.android.material.color.MaterialColors +import okhttp3.internal.toHexString + fun Context.copyToClipboard(text: String) { val clipboard = ContextCompat.getSystemService(this, ClipboardManager::class.java)!! @@ -20,4 +23,8 @@ fun Context.copyToClipboard(text: String) { fun Context.getDip(input: Float): Float { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, input, resources.displayMetrics) -} \ No newline at end of file +} + +fun Context.getDynamicColor(colorId: Int): String { + return "#" + MaterialColors.getColor(this, colorId, null).toHexString() +} diff --git a/app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt b/app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt index 45091aec4..059a17639 100644 --- a/app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt +++ b/app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt @@ -24,7 +24,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator -import com.widget.treeview.OnItemClickListener +import com.widget.treeview.OnTreeItemClickListener +import com.widget.treeview.TreeUtils.toNodeList import com.widget.treeview.TreeViewAdapter import dev.pranav.navigation.KtNavigationProvider import dev.pranav.navigation.NavigationProvider @@ -122,8 +123,7 @@ class EditorFragment : BaseBindingFragment() { } }) - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { binding.apply { @@ -141,14 +141,13 @@ class EditorFragment : BaseBindingFragment() { fileViewModel.removeAll() parentFragmentManager.popBackStack() } + } } - } - }) + }) TabLayoutMediator(binding.tabLayout, binding.pager, true, false) { tab, position -> tab.text = fileViewModel.files.value!![position].name tab.view.setOnLongClickListener { - Log.d("EditorFragment", "onLongClick: $position") showMenu(it, R.menu.tab_menu, position) true } @@ -159,7 +158,32 @@ class EditorFragment : BaseBindingFragment() { fileViewModel.files.observe(viewLifecycleOwner, ::handleFilesUpdate) fileViewModel.currentPosition.observe(viewLifecycleOwner) { pos -> - if (pos == -1) return@observe + binding.toolbar.apply { + if (pos == -1) { + menu.findItem(R.id.nav_items).apply { + isVisible = false + } + menu.findItem(R.id.undo).apply { + isVisible = false + } + menu.findItem(R.id.redo).apply { + isVisible = false + } + return@observe + } else { + menu.findItem(R.id.nav_items).apply { + isVisible = true + } + menu.findItem(R.id.undo).apply { + isVisible = true + } + menu.findItem(R.id.redo).apply { + isVisible = true + } + } + } + + if (binding.drawer.isOpen) binding.drawer.close() val tab = binding.tabLayout.getTabAt(pos) if (tab != null && tab.isSelected.not()) { @@ -175,11 +199,11 @@ class EditorFragment : BaseBindingFragment() { private fun initTreeView() { binding.included.recycler.apply { - val nodes = TreeViewAdapter.merge(project.root) + val nodes = project.root.toNodeList() layoutManager = LinearLayoutManager(context) adapter = TreeViewAdapter(context, nodes).apply { - setOnItemClickListener(object : OnItemClickListener { - override fun onItemClick(v: View, position: Int) { + setOnItemClickListener(object : OnTreeItemClickListener { + override fun onItemClick(view: View, position: Int) { val file = nodes[position].value if (file.exists().not() || file.isDirectory) return if (file.isFile) { @@ -187,8 +211,8 @@ class EditorFragment : BaseBindingFragment() { } } - override fun onItemLongClick(v: View, position: Int) { - showTreeViewMenu(v, nodes[position].value) + override fun onItemLongClick(view: View, position: Int) { + showTreeViewMenu(view, nodes[position].value) } }) } @@ -315,12 +339,10 @@ class EditorFragment : BaseBindingFragment() { val name = split[0].trim() val url = split[1].trim() - add( - object : Repository { - override fun getName() = name - override fun getURL() = url - } - ) + add(object : Repository { + override fun getName() = name + override fun getURL() = url + }) } } val artifact = try { @@ -469,8 +491,7 @@ class EditorFragment : BaseBindingFragment() { when (language) { is Language.Java -> { val psiJavaFile = FileFactoryProvider.getPsiJavaFile( - editor.file.name, - editor.editor.text.toString() + editor.file.name, editor.editor.text.toString() ) if (psiJavaFile.classes.isEmpty()) { @@ -504,8 +525,7 @@ class EditorFragment : BaseBindingFragment() { private fun showSymbols(navItems: List, editor: IdeEditor) { val binding = NavigationElementsBinding.inflate(layoutInflater) - binding.elementList.adapter = - NavAdapter(requireContext(), navItems, editor.text.indexer) + binding.elementList.adapter = NavAdapter(requireContext(), navItems, editor.text.indexer) val bottomSheet = BottomSheetDialog(requireContext()) binding.elementList.setOnItemClickListener { _, _, position, _ -> val item = navItems[position] @@ -655,11 +675,11 @@ class EditorFragment : BaseBindingFragment() { getCurrentFragment()?.hideWindows() navigateToCompileInfoFragment( file.absolutePath.replace( - project.srcDir.absolutePath + "/", - "" + project.srcDir.absolutePath + "/", "" ) ) } + R.id.create_kotlin_class -> { val binding = TreeviewContextActionDialogItemBinding.inflate(layoutInflater) binding.textInputLayout.suffixText = ".kt" diff --git a/app/src/main/kotlin/org/cosmicide/fragment/ProjectOutputFragment.kt b/app/src/main/kotlin/org/cosmicide/fragment/ProjectOutputFragment.kt index 7fb0a804c..a17c280b5 100644 --- a/app/src/main/kotlin/org/cosmicide/fragment/ProjectOutputFragment.kt +++ b/app/src/main/kotlin/org/cosmicide/fragment/ProjectOutputFragment.kt @@ -68,7 +68,6 @@ class ProjectOutputFragment : BaseBindingFragment() binding.infoEditor.apply { setEditorLanguage(TextMateLanguage.create("source.build", false)) - isWordwrap = true } binding.toolbar.title = "Running ${project.name}" diff --git a/app/src/main/kotlin/org/cosmicide/util/MaterialEditorTheme.kt b/app/src/main/kotlin/org/cosmicide/util/MaterialEditorTheme.kt new file mode 100644 index 000000000..19a18867e --- /dev/null +++ b/app/src/main/kotlin/org/cosmicide/util/MaterialEditorTheme.kt @@ -0,0 +1,42 @@ +/* + * This file is part of Cosmic IDE. + * Cosmic IDE is a free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * Cosmic IDE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with Cosmic IDE. If not, see . + */ + +package org.cosmicide.util + +import android.content.Context +import android.util.Log +import com.google.gson.Gson +import org.cosmicide.extension.getDynamicColor +import java.io.InputStream + +object MaterialEditorTheme { + private val gson = Gson() + + fun resolveTheme(context: Context, fileName: String): InputStream { + val theme = context.assets.open("textmate/$fileName") + return applyAttributes(theme, context) + } + + fun applyAttributes(stream: InputStream, context: Context): InputStream { + val contents = stream.bufferedReader().readText() + + val json = gson.fromJson(contents, Map::class.java) + // Should probably clean this up + ((json["settings"]!! as List>)[0]["settings"]!! as MutableMap).let { settings -> + settings["background"] = + context.getDynamicColor(com.google.android.material.R.attr.colorSurfaceContainerLow) + settings["foreground"] = + context.getDynamicColor(com.google.android.material.R.attr.colorOnSurfaceVariant) + settings["caret"] = + context.getDynamicColor(com.google.android.material.R.attr.colorOnSurfaceVariant) + } + Log.d("MaterialEditorTheme", "Applying attributes to theme") + Log.d("MaterialEditorTheme", json.toString()) + + return gson.toJson(json).byteInputStream() + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 035d97b94..93c88c125 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,8 +7,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.6.0-alpha07" apply false - id("com.android.library") version "8.6.0-alpha07" apply false + id("com.android.application") version "8.6.0" apply false + id("com.android.library") version "8.6.0" apply false id("org.jetbrains.kotlin.android") version "2.0.0" apply false id("org.jetbrains.kotlin.jvm") version "2.0.0" apply false id("dev.rikka.tools.materialthemebuilder") version "1.4.1" apply false diff --git a/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeUtils.kt b/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeUtils.kt new file mode 100644 index 000000000..e1ec13e00 --- /dev/null +++ b/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeUtils.kt @@ -0,0 +1,85 @@ +/* + * Copyright © 2022 Github Lzhiyong + * + * 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. + */ + +package com.widget.treeview + +import java.io.File + +data class Node( + var value: T, + var parent: Node? = null, + var children: MutableList> = mutableListOf(), + var isExpanded: Boolean = false, + var depth: Int = 0 +) { + override fun hashCode(): Int { + return value.hashCode() + } +} + +object TreeUtils { + + fun File.toNodeList(): MutableList> { + val files = listFiles()?.toMutableList() ?: return mutableListOf() + val dirs = files.filter { it.isDirectory }.sortedBy { it.name } + val remainingFiles = (files - dirs.toSet()).sortedBy { it.name } + return (dirs + remainingFiles).map { Node(it) }.toMutableList() + } + + fun addChildren( + parent: Node, + children: List> + ) { // Removed default value for slight performance gain + if (children.isNotEmpty()) { + parent.isExpanded = true + parent.children.addAll(children) + children.forEach { + it.parent = parent + it.depth = parent.depth + 1 + } + } + } + + fun removeChildren(parent: Node) { + if (parent.children.isNotEmpty()) { + parent.isExpanded = false + parent.children.forEach { child -> + child.parent = null + child.depth = 0 + if (child.isExpanded) { + child.isExpanded = false + removeChildren(child) + } + } + parent.children.clear() + } + } + + fun getDescendants(parent: Node): List> { + val descendants = mutableListOf>() + getDescendantsRecursive(parent, descendants) + return descendants + } + + private fun getDescendantsRecursive(parent: Node, descendants: MutableList>) { + descendants.addAll(parent.children) + parent.children.forEach { + if (it.isExpanded) { + getDescendantsRecursive(it, descendants) + } + } + } +} diff --git a/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeView.kt b/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeView.kt deleted file mode 100644 index 5054e5500..000000000 --- a/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeView.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright © 2022 Github Lzhiyong - * - * 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. - */ - -package com.widget.treeview - -data class Node( - var value: T, - var parent: Node? = null, - var child: List>? = null, - var isExpand: Boolean = false, - var level: Int = 0 -) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Node<*> - - return value == other.value && parent == other.parent && child == other.child && isExpand == other.isExpand && level == other.level - } - - override fun hashCode(): Int { - return value.hashCode() - } -} - -object TreeView { - - // add child node - fun add( - parent: Node, child: List>? = null - ) { - // check - child?.let { - if (it.isNotEmpty()) { - parent.isExpand = true - } - } - - parent.parent?.let { - val nodes = it.child - if (nodes != null && nodes.size == 1 && ((child != null && child.isEmpty()) || child == null)) { - parent.isExpand = true - } - } - - // parent associate with child - parent.child = child - - child?.forEach { - it.parent = parent - it.level = parent.level + 1 - } - } - - // remove child node - fun remove( - parent: Node, child: List>? = null - ) { - parent.child?.let { - if (it.isNotEmpty()) { - parent.isExpand = false - } - } - parent.child = null - - child?.forEach { childNode -> - childNode.parent = null - childNode.level = 0 - if (childNode.isExpand) { - childNode.isExpand = false - childNode.child?.let { listNodes -> - remove(childNode, listNodes) - } - } - } - } - - // Get all child nodes of the parent node - private fun getChildren( - parent: Node, result: MutableList> - ): List> { - parent.child?.let { result.addAll(it) } - - parent.child?.forEach { - if (it.isExpand) { - getChildren(it, result) - } - } - return result - } - - fun getChildren(parent: Node) = getChildren(parent, mutableListOf()) -} \ No newline at end of file diff --git a/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeViewAdapter.kt b/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeViewAdapter.kt index 0ba309713..2c47698a4 100644 --- a/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeViewAdapter.kt +++ b/feature/TreeView/src/main/kotlin/com/widget/treeview/TreeViewAdapter.kt @@ -23,58 +23,46 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.content.res.ResourcesCompat +import androidx.core.view.setPadding import androidx.recyclerview.widget.RecyclerView import com.unnamed.b.atv.R +import com.widget.treeview.TreeUtils.toNodeList import java.io.File -interface OnItemClickListener { - fun onItemClick(v: View, position: Int) - - fun onItemLongClick(v: View, position: Int) +interface OnTreeItemClickListener { + fun onItemClick(view: View, position: Int) + fun onItemLongClick(view: View, position: Int) } class TreeViewAdapter( - val context: Context, - var data: MutableList> + context: Context, + private var nodes: MutableList> ) : RecyclerView.Adapter() { - private val icFile = ResourcesCompat.getDrawable( + private val fileIcon = ResourcesCompat.getDrawable( context.resources, R.drawable.outline_insert_drive_file_24, context.theme ) - private val icFolder = ResourcesCompat.getDrawable( + private val folderIcon = ResourcesCompat.getDrawable( context.resources, R.drawable.outline_folder_24, context.theme ) - private val icChevronRight = ResourcesCompat.getDrawable( + private val chevronRightIcon = ResourcesCompat.getDrawable( context.resources, R.drawable.round_chevron_right_24, context.theme - ) - private val icExpandMore = ResourcesCompat.getDrawable( + )!! + private val expandMoreIcon = ResourcesCompat.getDrawable( context.resources, R.drawable.round_expand_more_24, context.theme - ) + )!! - private var listener: OnItemClickListener? = null - - companion object { - fun merge(root: File): MutableList> { - // child files - val list = root.listFiles()?.toMutableList() ?: return mutableListOf() - // dir with sorted - val dirs = list.filter { it.isDirectory }.sortedBy { it.name } - // file with sorted - val files = (list - dirs.toSet()).sortedBy { it.name } - // file to node - return (dirs + files).map { Node(it) }.toMutableList() - } - } + private var listener: OnTreeItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener?) { + fun setOnItemClickListener(listener: OnTreeItemClickListener?) { this.listener = listener } @@ -85,84 +73,78 @@ class TreeViewAdapter( } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val node = data[position] + val node = nodes[position] - // set itemView margin - holder.itemView.setPaddingRelative(node.level * 35, 0, 0, 0) + val indentation = node.depth * 36 + holder.itemView.setPaddingRelative(indentation, 0, 0, 0) if (node.value.isDirectory) { - if (!node.isExpand) { - holder.expandView.setImageDrawable(icChevronRight) - } else { - holder.expandView.setImageDrawable(icExpandMore) - } - - holder.fileView.setPadding(0, 0, 0, 0) - holder.fileView.setImageDrawable(icFolder) + holder.expandView.setImageDrawable(if (!node.isExpanded) chevronRightIcon else expandMoreIcon) + holder.fileView.setPadding(0) + holder.fileView.setImageDrawable(folderIcon) } else { - // non-directory not show the expand icon holder.expandView.setImageDrawable(null) - // padding - holder.fileView.setPadding(icChevronRight!!.intrinsicWidth, 0, 0, 0) - holder.fileView.setImageDrawable(icFile) + holder.fileView.setPaddingRelative(chevronRightIcon.intrinsicWidth, 0, 0, 0) + holder.fileView.setImageDrawable(fileIcon) } holder.textView.text = node.value.name holder.itemView.setOnClickListener { if (node.value.isDirectory) { - var parent = node - var child: List> - // expand and collapsed - if (!node.isExpand) { - var index = position - var count = 0 - // only one child directory - do { - child = merge(parent.value) - data.addAll(index + 1, child) - TreeView.add(parent, child) - - if (child.isNotEmpty()) { - parent = child[0] - count += child.size - index++ - } - } while (child.size == 1 && child[0].value.isDirectory) - // refresh data - notifyItemRangeInserted(position + 1, count) - } else { - child = TreeView.getChildren(parent) - data.removeAll(child.toSet()) - TreeView.remove(parent, parent.child) - // refresh data - notifyItemRangeRemoved(position + 1, child.size) - } - - // refresh data at position - notifyItemChanged(position) + toggleDirectory(node, position) } - - // callback listener?.onItemClick(it, position) } holder.itemView.setOnLongClickListener { - // callback listener?.onItemLongClick(it, position) - return@setOnLongClickListener true + true } } - override fun getItemViewType(position: Int) = position + private fun toggleDirectory(node: Node, position: Int) { + if (!node.isExpanded) { + expandDirectory(node, position) + } else { + collapseDirectory(node, position) + } + notifyItemChanged(position) + } - override fun getItemCount() = data.size + private fun expandDirectory(node: Node, position: Int) { + var parent = node + var children: List> + var index = position + var count = 0 + do { + children = parent.value.toNodeList() + nodes.addAll(index + 1, children) + TreeUtils.addChildren(parent, children) + + if (children.isNotEmpty()) { + parent = children[0] + count += children.size + index++ + } + } while (children.size == 1 && children[0].value.isDirectory) + notifyItemRangeInserted(position + 1, count) + } - class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { + private fun collapseDirectory(node: Node, position: Int) { + val descendants = TreeUtils.getDescendants(node) + nodes.removeAll(descendants.toSet()) + TreeUtils.removeChildren(node) + notifyItemRangeRemoved(position + 1, descendants.size) + } - val expandView: ImageView = v.findViewById(R.id.expand) - val fileView: ImageView = v.findViewById(R.id.file_view) - val textView: TextView = v.findViewById(R.id.text_view) + override fun getItemViewType(position: Int) = position + + override fun getItemCount() = nodes.size + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val expandView: ImageView = view.findViewById(R.id.expand) + val fileView: ImageView = view.findViewById(R.id.file_view) + val textView: TextView = view.findViewById(R.id.text_view) } } - diff --git a/feature/TreeView/src/main/res/layout/recycler_view_item.xml b/feature/TreeView/src/main/res/layout/recycler_view_item.xml index 42568d781..974fcb90d 100644 --- a/feature/TreeView/src/main/res/layout/recycler_view_item.xml +++ b/feature/TreeView/src/main/res/layout/recycler_view_item.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:paddingVertical="12dp"> + android:paddingVertical="16dp">