Unable to bring window to foreground with compose desktop - kotlin

With the following code the application window can be hidden using the button and restored using a global shortcut ALT+S.
Now I would like to also use the shortcut to bring the window to the foreground (if it wasn't hidden).
Find below my failed attempt to do so. (I am relatively new to the matter of jetpack compose.)
var windowVisible = mutableStateOf(true)
#Composable
fun App(windowFocusRequester: FocusRequester) {
MaterialTheme() {
Button(modifier = Modifier.focusRequester(windowFocusRequester), onClick = {
println("click to hide received")
windowVisible.value = false
}) {
Text("Hide window (ALT+S to show)")
}
}
}
fun main() = application() {
Window(onCloseRequest = ::exitApplication, visible = windowVisible.value, focusable = true,
) {
val windowFocusRequester = remember { FocusRequester() }
val provider = Provider.getCurrentProvider(false)
provider.register(
KeyStroke.getKeyStroke("alt S")
) {
println("shortcut to show received")
windowVisible.value = true
windowFocusRequester.requestFocus()
}
App(windowFocusRequester)
}
}
Probably you would need to add the FocusRequester as a modifier to the Window but this does not seem to be possible.
To be able to run the code this lib is needed
implementation("com.github.tulskiy:jkeymaster:1.3")
Thanks for any ideas to try, advance or even workaround! (maybe accessing awt window?)

Found a better solution (without the flickering): using alwaysOnTop, inspired by this answer... (does not work without the state-thingy)
var windowVisible = mutableStateOf(true)
#Composable
fun App() {
MaterialTheme() {
Button(onClick = {
println("click to hide received")
windowVisible.value = false
}) {
Text("Hide window (ALT+S to show)")
}
}
}
fun main() = application() {
val windowAlwaysOnTop = remember { mutableStateOf(false) }
val state = rememberWindowState(width = 320.dp, height = 200.dp)
Window(
onCloseRequest = ::exitApplication, state = state, visible = windowVisible.value,
alwaysOnTop = windowAlwaysOnTop.value, focusable = true,
) {
val provider = Provider.getCurrentProvider(false)
provider.register(
KeyStroke.getKeyStroke("alt S")
) {
println("shortcut to show received")
windowVisible.value = true
windowAlwaysOnTop.value = true
windowAlwaysOnTop.value = false
}
App()
}
}

I came across the same issue and ended up with this solution:
import androidx.compose.runtime.*
import androidx.compose.ui.window.FrameWindowScope
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
#Composable
fun FrameWindowScope.WindowFocusRequester(state: WindowState): () -> Unit {
var requestFocus by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
var showFakeWindow by remember { mutableStateOf(false) }
if (requestFocus) {
requestFocus = false
showFakeWindow = true
scope.launch {
delay(1)
showFakeWindow = false
}
}
if (showFakeWindow) {
Window({}) {}
window.toFront()
state.isMinimized = false
}
return { requestFocus = true }
}
Use the code like this:
fun main() = application {
val state = rememberWindowState()
Window({ exitApplication() }, state) {
val requestWindowFocus = WindowFocusRequester(state)
// Inside some listener just invoke the returned lambda
requestWindowFocus()
}
}
It is a hack. So maybe it will not work on every OS. I tested it with Ubuntu 20 and Compose 1.1.1.
If this is not working for you, try to increase the delay duration or exchange the toFront() call with something mentioned here.

Related

Wait for result from Coroutine and then use it in Composable function

I am creating a video scraper, and it has the following function which scrapes the video source from a URL that has been given as the parameter:
fun scrapeVideoSrcFromUrl(url: String): String? {
val document = Jsoup.connect(url).get()
for (element in document.getElementsByTag("script")) {
if (element.attr("type") == "application/ld+json") {
val content = element.data()
val array = JsonParser.parseString(content).asJsonArray
val embedUrl = Gson().fromJson(array.get(0).asJsonObject.get("embedUrl"), String::class.java)
var embedId = ""
for (char in embedUrl.dropLast(1).reversed()) {
if (char != '/') {
embedId += char
} else {
break
}
}
val doc = Jsoup.connect("$RUMBLE_API_URL${embedId.reversed()}").ignoreContentType(true).get()
val jsonData = doc.getElementsByTag("body").first()?.text()
val mp4 = JsonParser.parseString(jsonData).asJsonObject.get("u").asJsonObject.get("mp4").asJsonObject.get("url").toString()
return mp4.replace("\"", "")
}
}
return null
}
I want to show this in a dialog for a certain link using ExoPlayer, so I did the following:
#Composable
fun VideoPlayer(videoSrc: String) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(
MediaItem.fromUri(
videoSrc
)
)
prepare()
playWhenReady = true
}
}
Box(modifier = Modifier.fillMaxSize()) {
DisposableEffect(key1 = Unit) {
onDispose {
exoPlayer.release()
}
}
AndroidView(
factory = {
StyledPlayerView(context).apply {
player = exoPlayer
layoutParams =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
)
}
}
Then, in the main Composable:
if (openDialog) {
AlertDialog(
onDismissRequest = {
openDialog = false
},
title = {
Column {
Text(
text = viewModel.currentRumbleSearchResult?.title ?: ""
)
Spacer(
Modifier.height(8.dp)
)
Text(
text = "By ${viewModel.currentRumbleSearchResult?.channel?.name ?: ""}",
style = MaterialTheme.typography.titleSmall
)
}
},
text = {
VideoPlayer(RumbleScraper.create().scrapeVideoSrcFromUrl("https://rumble.com/v1m9oki-our-first-automatic-afk-farms-locals-minecraft-server-smp-ep3-live-stream.html")!!)
},
confirmButton = {
TextButton(
onClick = {
openDialog = false
}
) {
Text("Exit")
}
}
)
}
After running that code I keep getting NetworkOnMainThread exceptions, and I tried many things to fix it but nothing worked.
So I am unsure what to do as to how I can go around fixing this. I was wondering how I would go around waiting in the background for a result and then show it in the Compose function when it returns the value?
You can do something like this:
var videoSrc by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
videoSrc = RumbleScraper.create().scrapeVideoSrcFromUrl("")
}
}
text = { VideoPlayer(videoSrc) }
You can also call the scrapeVideoSrcFromUrl inside your viewModel and update some state that you will use in UI.
If you want to run it in response to some event like item click, you will be better of with something like this:
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
withContext(Dispatchers.IO) { ... }
}
}
)

Coroutines not working in jetpack Compose

I use the following way to get network data.
I start a network request in a coroutine but it doesn't work, the pagination load is not called.
But if I call the network request through the init method in the ViewModel I can get the data successfully.
#Composable
fun HomeView() {
val viewModel = hiltViewModel<CountryViewModel>()
LaunchedEffect(true) {
viewModel.getCountryList() // Not working
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
Scaffold {
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 96.dp),
verticalArrangement = Arrangement.spacedBy(32.dp),
modifier = Modifier.fillMaxSize()) {
items(pagingItems) { countryGroup ->
if (countryGroup == null) return#items
Text(text = "Hello", modifier = Modifier.height(100.dp))
}
}
}
}
#HiltViewModel
class CountryViewModel #Inject constructor() : ViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
init {
getCountryList() // can work
}
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource()
}.flow.cachedIn(viewModelScope)
}
}
I don't understand what's the difference between the two calls, why doesn't it work?
Any helpful comments and answers are greatly appreciated.
I solved the problem, the coroutine was used twice in the code above, which caused network data to not be fetched.
A coroutine is used here:
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource()
}.flow.cachedIn(viewModelScope)
}
Here is another coroutine:
LaunchedEffect(true) {
viewModel.getCountryList() // Not working
}
current usage:
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
viewModel.getCountryList()
execute.value = false
}

How to navigate back to the correct one screen before the previous in jetpack compose?

I have four compose screens and by clicking on their items user leads to my AdShowScreen then after watching the ad they lead to the FinalShow screen. like the image below
Now I want to navigate correctly back from finalShowScreen to one of the four compose screens that came from by overriding the back press button in FinalShowScreen.
This is my navGraph.
#SuppressLint("UnrememberedMutableState")
#Composable
fun MyNavGraph(
navController: NavHostController) {
val actions = remember(navController) { MainActions(navController) }
NavHost(
navController = navController,
startDestination = BottomNavItems.First.route
) {
composable(BottomNavItems.First.route) {
FirstScreen(actions)
}
composable(BottomNavItems.Second.route) {
SecondScreen(navController, actions)
}
composable(BottomNavItems.Third.route) {
ThirdScreen()
}
composable(Screens.Fourth.route) {
FourthScreen(navController, actions)
}
composable("${Screens.FinalShow.route}/{maskArg}") {
val maskArg = it.arguments?.getString("maskArg")
if (maskArg != null) {
FinalShowScreen(
maskArg = maskArg, navController,actions
)
}
}
composable("${Screens.RewardedShow.route}/{maskArg}") {
val maskArg = it.arguments?.getString("maskArg")
if (maskArg != null) {
RewardedShowCompose(
maskArg = maskArg, navController = navController, actions = actions
)
}
}
}
}
class MainActions(navController: NavController) {
val goToRoute: (String) -> Unit = { route ->
navController.navigate(route) {
navController.graph.startDestinationRoute?.let { rout ->
popUpTo(rout) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
}
I'm trying this code below but It doesn't work. it goes back to AdShowScreen
val gotoAdShow: (String, String) -> Unit = { maskArg, route ->
navController.navigate("$route/$maskArg") {
navController.graph.startDestinationRoute?.let { rout ->
popUpTo(rout) {
saveState = true
inclusive = true
}
}
launchSingleTop = true
restoreState = true
}
}
Why are you using
navController.navigate(route) {
navController.graph.startDestinationRoute?.let { rout ->
popUpTo(rout) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
What if instead you use navHostController.popBackStack("ad", inclusive = true) directly?
To navigate back from the final screen to the first one, add this to the FourthScreen Composable;
BackHandler {
navController.navigate(BottomNavItems.First.route) // Or to whichever route you want to navigate to
}
Also, it is not a good practice to pass the navController to other Composables because it will make your UI pretty hard to test.

How can I make two windows on jetpack compose desktop and going from window to another?

How can I make two windows on jetpack compose desktop and going from window to another when I click button for example?
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Products Manager",
state = rememberWindowState(width = 700.dp, height = 600.dp)
) {
val count = remember { mutableStateOf(0) }
MaterialTheme {
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Button(modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
count.value++
}) {
Text(if (count.value == 0) "Hello World" else "Clicked ${count.value}!")
}
Button(modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
count.value = 0
}) {
Text("Reset")
}
}
}
}
}
To create multiple windows, you simply need to have multiple Window composables. Check out Open and close multiple windows documentation section for example.
To switch between windows programmatically, you can use window.toFront() on the window that should become topmost: window is property available in FrameWindowScope inside Window.content.
Here's an example how it can be done with two window "types". You can replace type with any other identifier.
enum class WindowTypes {
First,
Second,
}
fun main() = application {
val windowFocusRequestSharedFlow = remember { MutableSharedFlow<WindowTypes>() }
WindowTypes.values().forEach { windowType ->
key(windowType) {
Window(
title = windowType.toString(),
onCloseRequest = ::exitApplication,
) {
LaunchedEffect(Unit) {
windowFocusRequestSharedFlow
.filter { it == windowType }
.collect {
window.toFront()
}
}
val scope = rememberCoroutineScope()
Button({
scope.launch {
val windowTypeToFocus = WindowTypes.values().run {
get((indexOf(windowType) + 1) % count())
}
windowFocusRequestSharedFlow.emit(windowTypeToFocus)
}
}) {
Text("next window")
}
}
}
}
}

How can I successfully pass my LiveData from my Repository to my Compose UI using a ViewModel?

I am trying to pass live events from a Broadcast Receiver to the title of my Homepage.
I am passing a String from the Broadcast Receiver to my Repository successfully, but in the end my title is always null. What am I missing?
My Repository looks like this:
object Repository {
fun getAndSendData (query: String): String{
return query
}
}
Then in my ViewModel I have:
private val _data = MutableLiveData<String>()
val repoData = _data.switchMap {
liveData {
emit(Repository.getAndSendData(it))
}
}
And finally in my Composable I have:
val repoData = viewModel.repoData.observeAsState()
topBar = {
TopAppBar(
title = { Text(text = if (repoData.value == null)"null" else repoData.value!!, style = typography.body1) },
navigationIcon = {
IconButton(onClick = { scaffoldState.drawerState.open() }) {
Icon(Icons.Rounded.Menu)
}
}
)
},
I don't think we can use Live Data from the compose(jetpack) because it can run from the Main thread. I used onCommit() {} with interface from the compose.
onCommit() {
viewModel.testCountriesList(object : NetworkCallBack {
override fun test(response: GeneralResponse) {
if(response.code == 200) {
counties = response.response
responseState.value = true
} else {
responseState.value = false
}
}
})
}