Skip to content

IDE-51 Extend "Search" feature to names of scenes, groups, objects, looks, and sounds #5057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2025 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* An additional term exception under section 7 of the GNU Affero
* General Public License, version 3, is available at
* http://developer.catrobat.org/license_additional_term
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.catrobat.catroid.test.ui
import android.view.KeyEvent
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.catrobat.catroid.ProjectManager
import org.catrobat.catroid.R
import org.catrobat.catroid.content.Project
import org.catrobat.catroid.content.Scene
import org.catrobat.catroid.content.Sprite
import org.catrobat.catroid.ui.FinderDataManager
import org.catrobat.catroid.ui.MainMenuActivity
import org.catrobat.catroid.uiespresso.ui.fragment.actionutils.ActionUtils
import org.hamcrest.Matchers.`is`
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.catrobat.catroid.content.Script
import org.catrobat.catroid.content.StartScript
import org.catrobat.catroid.content.bricks.FinishStageBrick

@RunWith(AndroidJUnit4::class)
class SearchTest {
var projectName = "searchTestProject"
private val projectManager = ProjectManager.getInstance()

@Rule
@JvmField
var baseActivityTestRule = ActivityScenarioRule(MainMenuActivity::class.java)
private lateinit var script: Script

@Before
fun setUp() {
createProject(projectName)
}

@Test
fun testSearchMixed() {
val queryString = "test"
val expectedResults = arrayOf("testsprite2","finish tests","testlook3","testlook4",
"testsound3","testsound4","testsprite3","testscene2",
"testsprite5","testscene3")
Espresso.onView(withId(R.id.myProjectsTextView)).perform(click())
Espresso.onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click())
)

Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation()
.targetContext)
Espresso.onView(withText(R.string.search)).perform(click())

Espresso.onView(withId(R.id.search_bar)).perform(click())

val viewMatcher = Espresso.onView(withId(R.id.search_bar)).perform(
ViewActions.replaceText(queryString)
)
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
viewMatcher.perform(ViewActions.pressKey(KeyEvent.KEYCODE_ENTER))

for (i in expectedResults.indices) {
val currentFoundName = FinderDataManager.instance.getSearchResultsNames()?.get(i)
assertThat(currentFoundName, `is`(expectedResults[i]))
Espresso.onView(withId(R.id.find_next)).perform(click())
}
for (i in expectedResults.indices.last downTo 0) {
val currentFoundName = FinderDataManager.instance.getSearchResultsNames()?.get(i)
assertThat(currentFoundName, `is`(expectedResults[i]))
Espresso.onView(withId(R.id.find_previous)).perform(click())
}
}

@Test
fun testSceneNotFindableOnLowerLevel(){

val queryString = "scene1"

Espresso.onView(withId(R.id.myProjectsTextView)).perform(click())

Espresso.onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click())
)
Espresso.onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click())
)

Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext)
Espresso.onView(withText(R.string.search)).perform(click())
Espresso.onView(withId(R.id.search_bar)).perform(click())

val viewMatcher = Espresso.onView(withId(R.id.search_bar)).perform(
ViewActions.replaceText(queryString)
)
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
viewMatcher.perform(ViewActions.pressKey(KeyEvent.KEYCODE_ENTER))

assertThat(FinderDataManager.instance.currentMatchIndex, `is` (-1))
}
@Test
fun testSpriteNotFindableOnLowerLevel(){

val queryString = "sprite"

Espresso.onView(withId(R.id.myProjectsTextView)).perform(click())

Espresso.onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click())
)
Espresso.onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click())
)
Espresso.onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click())
)

Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext)
Espresso.onView(withText(R.string.search)).perform(click())
Espresso.onView(withId(R.id.search_bar)).perform(click())

val viewMatcher = Espresso.onView(withId(R.id.search_bar)).perform(
ViewActions.replaceText(queryString)
)
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
viewMatcher.perform(ViewActions.pressKey(KeyEvent.KEYCODE_ENTER))

assertThat(FinderDataManager.instance.currentMatchIndex, `is` (-1))
}

fun createProject(projectName: String?) {
val project = Project(ApplicationProvider.getApplicationContext(), projectName)
projectManager.currentProject = project
project.sceneList.clear()
val scene1Name = "scene1"
val scene2Name = "testScene2"
val scene3Name = "testScene3"
val scene4Name = "scene4"

val scene1 = Scene(scene1Name, project)
val scene2 = Scene(scene2Name, project)
val scene3 = Scene(scene3Name, project)
val scene4 = Scene(scene4Name, project)
val sprite1 = Sprite("background")
val sprite2 = Sprite("testSprite2")
val sprite3 = Sprite("testSprite3")
val sprite4 = Sprite("background")
val sprite5 = Sprite("testSprite5")
project.addScene(scene1)
project.addScene(scene2)
project.addScene(scene3)
project.addScene(scene4)
project.getSceneByName(scene1Name).addSprite(sprite1)
project.getSceneByName(scene1Name).addSprite(sprite2)
project.getSceneByName(scene1Name).addSprite(sprite3)
project.getSceneByName(scene2Name).addSprite(sprite4)
project.getSceneByName(scene2Name).addSprite(sprite5)

projectManager.apply {
setCurrentSceneAndSprite(scene1Name, sprite2.name)
currentSprite = sprite2 // Force update
currentlyEditedScene = project.getSceneByName(scene1Name)
}

script = StartScript()
script.addBrick(FinishStageBrick())
projectManager.currentSprite.addScript(script)

ActionUtils.addSound(projectManager,"testSound3")
ActionUtils.addSound(projectManager, "Sound3")
ActionUtils.addSound(projectManager,"testSound4")
ActionUtils.addSound(projectManager, "Sound4")
ActionUtils.addLook(projectManager,"testLook3")
ActionUtils.addLook(projectManager, "Look3")
ActionUtils.addLook(projectManager,"testLook4")
ActionUtils.addLook(projectManager, "Look4")

ProjectManager.getInstance().currentProject = project
}
}
161 changes: 161 additions & 0 deletions catroid/src/main/java/org/catrobat/catroid/ui/BaseFinderFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2025 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* An additional term exception under section 7 of the GNU Affero
* General Public License, version 3, is available at
* http://developer.catrobat.org/license_additional_term
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.catrobat.catroid.ui

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import org.catrobat.catroid.ProjectManager
import org.catrobat.catroid.R
import org.catrobat.catroid.common.Nameable
import org.catrobat.catroid.content.Project
import org.catrobat.catroid.content.Scene
import org.catrobat.catroid.content.Sprite
import org.catrobat.catroid.ui.recyclerview.fragment.RecyclerViewFragment
import org.koin.android.ext.android.inject

abstract class BaseFinderFragment<T : Nameable?> : RecyclerViewFragment<T>() {

protected val projectManager: ProjectManager by inject()
protected lateinit var currentProject: Project
protected lateinit var currentScene: Scene
protected lateinit var currentSprite: Sprite

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val parentView = super.onCreateView(inflater, container, savedInstanceState)
activity = getActivity() as SpriteActivity?
recyclerView = parentView!!.findViewById(R.id.recycler_view)
currentProject = projectManager.currentProject
currentScene = projectManager.currentlyEditedScene
currentSprite = projectManager.currentSprite

setupFinderListeners()

if (FinderDataManager.instance.getInitiatingFragment() != FinderDataManager.FragmentType.NONE) {
val sceneAndSpriteName = createActionBarTitle(1)
finder?.onFragmentChanged(sceneAndSpriteName)
scrollToSearchResult()
}

return parentView
}

private fun setupFinderListeners() {
finder?.setOnResultFoundListener(object : Finder.OnResultFoundListener {
override fun onResultFound(
sceneIndex: Int,
spriteIndex: Int,
brickIndex: Int,
type: Int,
totalResults: Int,
textView: TextView?
) {
currentProject = projectManager.currentProject
currentScene = currentProject.sceneList[sceneIndex]
FinderDataManager.instance.type = type

if (type == FinderDataManager.FragmentType.SPRITE.id) {
textView?.text = createActionBarTitle(2)
} else {
currentSprite = currentScene.spriteList[spriteIndex]
textView?.text = createActionBarTitle(1)
}

FinderDataManager.instance.currentMatchIndex = brickIndex

when (type) {
1 -> activity.onBackPressed()
2 -> {
projectManager.setCurrentlyEditedScene(currentScene)
activity.onBackPressed()
}
3 -> {
projectManager.setCurrentSceneAndSprite(currentScene.name, currentSprite.name)
activity.loadFragment(0)
}
4 -> {
projectManager.setCurrentSceneAndSprite(currentScene.name, currentSprite.name)
activity.loadFragment(1)
}
5 -> {
projectManager.setCurrentSceneAndSprite(currentScene.name, currentSprite.name)
activity.loadFragment(2)
}
}

hideKeyboard()
}
})

finder?.setOnCloseListener(object : Finder.OnCloseListener {
override fun onClose() {
finishActionMode()
if (!activity.isFinishing) {
activity.setCurrentSceneAndSprite(
projectManager.currentlyEditedScene,
projectManager.currentSprite
)
activity.supportActionBar?.title = createActionBarTitle(1)
activity.addTabs()
}
activity.findViewById<View>(R.id.toolbar).visibility = View.VISIBLE
}
})

finder?.setOnOpenListener(object : Finder.OnOpenListener {
override fun onOpen() {
if (FinderDataManager.instance.getInitiatingFragment() == FinderDataManager.FragmentType.NONE) {
finder.setInitiatingFragment(getFragmentType())
FinderDataManager.instance.setSearchOrder(getSearchOrder())
}
activity.removeTabs()
activity.findViewById<View>(R.id.toolbar).visibility = View.GONE
}
})
}

fun createActionBarTitle(flag: Int): String {
return if(flag == 1) {
if (currentProject.sceneList != null && currentProject.sceneList.size == 1) {
currentSprite.name
} else {
currentScene.name + ": " + currentSprite.name
}
} else{
currentScene.name
}
}

private fun hideKeyboard() {
val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(requireView().windowToken, 0)
}

protected abstract fun getFragmentType(): FinderDataManager.FragmentType
protected abstract fun getSearchOrder(): Array<Int>
}
Loading