Textual (python) - how to add click event in simple Text object? - python

I'm trying to get it so I can add links in text rendered by Textual.
My text may have multiple links, for example:
Hello [#click=hello]World[/] there, how are you?
This is a test of [#click=more] more info[/] being clickable as well.
In this simple sample I made, clicking on the word "World" should hopefully change the background color to red, but it doesn't work.
NOTE: I also bound the "b" key to do pretty much the same thing, so I could see it work
It should change the background color, and the subtitle of the app.
import os
import sys
from rich.console import RenderableType
from rich.panel import Panel
from rich.text import Text
from textual.app import App
from textual.widgets import Header, Footer, ScrollView
from textual.widgets import Placeholder
class MyApp(App):
async def on_load(self) -> None:
await self.bind("b", "color('blue')")
async def on_mount(self) -> None:
await self.view.dock(Header(), size=5, edge="top")
await self.view.dock(Footer(), edge="bottom")
await self.view.dock(ScrollView(Panel("Hello [#click=hello]World[/] more info here")), edge="top")
async def action_color(self, color:str) -> None:
self.app.sub_title = "KEYBOARD"
self.background = f"on {color}"
async def action_hello(self) -> None:
self.app.sub_title = "CLICKED"
self.background = "on red"
MyApp.run(title="Test click", log="textual.log")
I asked this same question in the textual discussions and originally rich discussions, but haven't been able to see how to make this work from the feedback I received there, which was helpful for sure, but I'm missing something here, so thanks for any input.

It seems like #click=action doesn't work in textual (at least I couldn't make it work at all).
After digging up in rich documentation, I stepped upon Text class. That one, has on method, that can create click callback.
It supports __add__, so You can concat multiple Text(s) together with + operator.
Second piece of the puzzle was to find out what to set as a click callback. Again I looked in the source code and found _action_targets in app.py. That contains {"app", "view"} set.
Putting all together, You can create link(s) using Text with on(click="app.callback()"), which will call action_callback method of MyApp Class (instance of textual App). Then creating final panel text by concating other Text(s) and link(s) together.
Here is working example, turning background to red clicking on Hello or green clicking on World.
from rich.panel import Panel
from rich.text import Text
from textual.app import App
from textual.widgets import Header, Footer, ScrollView
class MyApp(App):
async def on_load(self) -> None:
await self.bind("b", "color('blue')")
async def on_mount(self) -> None:
await self.view.dock(Header(), size=5, edge="top")
await self.view.dock(Footer(), edge="bottom")
link1 = Text("Hello").on(click="app.hello()")
link2 = Text("World").on(click="app.world()")
panel_text = link1 + " " + link2 + Text(" more info here")
await self.view.dock(ScrollView(Panel(panel_text)), edge="top")
async def action_color(self, color: str) -> None:
self.app.sub_title = "KEYBOARD"
self.background = f"on {color}"
async def action_hello(self) -> None:
self.app.sub_title = "CLICKED Hello"
self.background = "on red"
async def action_world(self) -> None:
self.app.sub_title = "CLICKED World"
self.background = "on green"
MyApp.run(title="Test click", log="textual.log")
I know it's not ideal solution, but it's closest I could get to what You want.

You can also make a text with different properties with Text.assemble():
async def on_mount(self) -> None:
await self.view.dock(Header(), size=5, edge="top")
await self.view.dock(Footer(), edge="bottom")
panel_text = Text.assemble(
"Hello ",
Text("World","dark_blue u").on({"#click" : "app.hello()"}),
" more ",
Text("info","dark_blue u").on({"#click" : "app.color('blue')"}),
" here")
await self.view.dock(ScrollView(Panel(panel_text)), edge="top")

Related

How to reset Discord.OptionSelect in Python Pycord?

I am trying to implement a "Back" button that will return to the previous dropdown.
I am using Pycord.
I have a dropdown with options to pick different food categories, after you pick a category, the dropdown menu changes to a new dropdown where see items in that category.
In addition, you have a "Back" button that should get you to the previous dropdown.
At the moment I get In components.0.components.0.options.1: The specified option value is already used Error, after I click the back button and click the same category again.
Here is where I recreate the issue, first I run the slash command /shop and I click the "Meats" category
Then I get to a new dropdown and I click the "Back" Button:
I get to the original dropdown.
and if I click the "Meats" cateogry again it crushes.
main.py
import discord
from example1 import CategoryView
bot = discord.Bot()
#bot.command()
async def shop(ctx):
await ctx.respond("Choose a category from the dropdown below! ✨", view=CategoryView())
#bot.event
async def on_ready():
print("Ready!")
bot.run("TOKEN")
example1.py
import discord
from example2 import CategoryPriceView
class CategoryView(discord.ui.View):
def __init__(self):
super().__init__()
#discord.ui.select(
placeholder = "Choose a food category!",
min_values = 1,
max_values = 1,
options = [
discord.SelectOption(
label="Meats",
emoji='🍖'
),
discord.SelectOption(
label="Salads",
emoji='🥗'
)
]
)
async def select_callback(self, select, interaction):
category = select.values[0]
await interaction.response.edit_message(content="Choose your item!", view=CategoryPriceView(category, self))
example2.py
import discord
options = []
class CategoryPriceView(discord.ui.View):
def __init__(self, category, father):
super().__init__()
global options
self.category = category
self.father = father
if self.category == 'Meats':
options.append(discord.SelectOption(label='Steak', description="Price: 40$"))
elif self.category == 'Salads':
options.append(discord.SelectOption(label='Greek Salad', description="Price: 30$"))
#discord.ui.button(label="Back", row=1, style=discord.ButtonStyle.blurple)
async def button_callback(self, button, interaction):
await interaction.response.edit_message(content="Choose a category from the dropdown below! ✨", view=self.father)
#discord.ui.select(
placeholder = "Choose an item!",
min_values = 1,
max_values = 1,
options = options
)
async def select_callback(self, select, interaction):
item = select.values[0]
await interaction.response.edit_message(content=f"You chose {item}! ", view=None)
EDIT: My answer was completely wrong. Edited to provide actual answer.
The issue is the global options. Every time you select an option from the original CategoryView, you're creating a new instance of CategoryPriceView and adding to options. As options is global and never gets cleared - there are duplicates in there - hence the duplicate value error. You can resolve this by subclassing Select and creating a custom CategoryPriceSelect that has the options logic you want. See below.
# example2.py
import discord
class CategoryPriceSelect(discord.ui.Select):
def __init__(self, category: str) -> None:
options = []
if category == 'Meats':
options.append(discord.SelectOption(label='Steak', description="Price: 40$"))
elif category == 'Salads':
options.append(discord.SelectOption(label='Greek Salad', description="Price: 30$"))
super().__init__(
placeholder="Choose an item!",
min_values=1,
max_values=1,
options=options
)
async def callback(self, interaction: discord.Interaction):
item = self.values[0]
await interaction.response.edit_message(content=f"You chose {item}! ", view=None)
class CategoryPriceView(discord.ui.View):
def __init__(self, category, father):
super().__init__()
self.category = category
self.father = father
select = CategoryPriceSelect(self.category)
self.add_item(select)
#discord.ui.button(label="Back", row=1, style=discord.ButtonStyle.blurple)
async def button_callback(self, button, interaction):
await interaction.response.edit_message(content="Choose a category from the dropdown below! ✨", view=self.father)
I've tested this and this works now without throwing the error and I believe this has your desired functionality.

Discord Prediction Bot

I'm trying to create a bot that my friends and I can do predictions through for the upcoming Soccer World Championship.
Now I want this bot to be able to ask the result of each match as a question, with a button for answer.
Creating one question works perfectly, but then moving on to the next question is what I have issues with.
I found the following online:
"Each message is a different view, but you don't need to program each view. You can create a class that inherits from discord.ui.button where it takes a team name as a string, then pass that button (plus another for the 2nd team) to a class that inherits from discord.ui.View"
I'm a beginner when it comes to python and I have no clue how I actually do this. Any tips?
What I want:
Message 1: "Team A vs Team B"
Button 1: Team A
Button 2: Team B
Message 2: "Team C vs Team C"
Button 1: Team C
Button 2: Team D
The code I currently already have:
class WorldsGeneral(discord.ui.View):
def __init__(self, *, timeout=10):
super().__init__(timeout=timeout)
#discord.ui.button(label="Groups", style=discord.ButtonStyle.red)
async def Finals(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message()
# The messages for predictions would have to be send by pressing this button ^
#discord.ui.button(label="Standings")
async def Standings(
self, interaction: discord.Interaction, button: discord.ui.Button
):
await interaction.response.send_message()
#bot.command(aliases=["poule", "predictions])
async def worlds(ctx):
embed = discord.Embed(
colour=0x0433f3,
title=":trophy: Worlds Poule 2022 :trophy:",
)
embed.set_image(
url=f"https://thumbs.gfycat.com/AccomplishedAdorableClumber-size_restricted.gif"
)
view = WorldsGeneral()
msg = await ctx.send(
":construction: Major Work In Progress :construction:", view=view
)
view.orig_mes = msg
You have an issue with your worlds command: there's a missing quotation at the very top. Here's how the fixed code should look:
#bot.command(aliases=["poule", "predictions"]) # Missing quotation right here
async def worlds(ctx):
embed = discord.Embed(
colour=0x0433f3,
title=":trophy: Worlds Poule 2022 :trophy:",
)
embed.set_image(
url=f"https://thumbs.gfycat.com/AccomplishedAdorableClumber-size_restricted.gif"
)
view = WorldsGeneral()
msg = await ctx.send(
":construction: Major Work In Progress :construction:", view=view
)
view.orig_mes = msg

Slash command autocompletion in pycord

I want to start working with SlashCommand autocompletion in pycord. Since I think that my request in the previous question is impossible, I thought to myself that maybe I can change the autocomplete based on the user's choice in one of the slash-command entries and display the desired entry to the user earlier.
I'm asking this because I don't found any good resource for learning this and I don't understand docs
there is an example from the Pycord documents, I think if you experiment with this part of the code, you will understand how it can work
https://github.com/Pycord-Development/pycord/blob/master/examples/app_commands/slash_autocomplete.py
If you want use them in cogs
there's my part for cogs
import discord
from discord import option
from discord.ext import commands
mycolors = ["red", "white", "yellow"]
class ExampleAutocompletion(commands.Cog):
ctx_parse = discord.ApplicationContext
def __init__(self, bot: discord.Bot):
self.bot = bot.user
#staticmethod
def colorAutocomplete(self: discord.AutocompleteContext):
return mycolors
#staticmethod
def flowertypeAutocomplete(self: discord.AutocompleteContext):
chosen_color = self.options["color"]
match chosen_color:
case "red":
return ["Rose", "Georgina"]
case "white":
return ["Chrisantem", "Gortensia"]
case "yellow":
return ["Sunflower", "Narciss"]
case _:
return ["There is no flower with this color"]
#commands.slash_command(
guild_ids=["Your Guild ID for test"],
description="")
#option("color", description="What's your favourite color?",
autocomplete=colorAutocomplete)
#option("flowertype", description="and that's your flower shape!",
autocomplete=flowertypeAutocomplete)
async def beautyflowers(self, ctx: ctx_parse,
color: str, flowertype: str):
await ctx.respond(f"My flower is {flowertype} and its color is {color}!")
def setup(bot):
bot.add_cog(ExampleAutocompletion(bot))

Multiple Button not showing up in nextcord

I'm currently coding a quizz bot and I came up with this code:
class answers(nextcord.ui.View):
def __init__(self, ans1, ans2, ans3):
super().__init__(timeout=None)
self.ans1 = ans1
self.ans2 = ans2
self.ans3 = ans3
self.value = False
#nextcord.ui.button(label = '1', custom_id="1", style = nextcord.ButtonStyle.red)
async def reaction(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
await interaction.response.send_message('ans1')
self.value = True
self.stop()
#nextcord.ui.button(label='2', custom_id='2' )
async def reaction(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
await interaction.response.send_message('ans2')
self.value = True
self.stop()
#nextcord.ui.button(label='3', custom_id='3')
async def reaction(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
await interaction.response.send_message('ans3')
self.value = True
self.stop()
This is the class that should display 3 buttons. However it displays only the third one. I do not get any errors.
How can I write this code so it does display all three?
You'll need to use different method names for each of the buttons. Changing the names to reaction1, reaction2, reaction3 would fix your issue.
The reason is the ui.button decorator doesn't store the button info elsewhere, instead it attaches the relevant info onto the method and returns the method back. So having the same name for the three method, you're actually reassigning answers.reaction every time you define a button, and only the last one gets kept. discord.py only evaluates all of the components from a view at runtime, so the last button is all it sees—thus it only displays the last button.

Python threading in for loop

I am creating a python script that grabs information from an API and creates a context menu that gives you access to them. I want to use threading as it runs a little slow on the one call to the API, but I am not sure how to implement threading with my code. I am using this site for threading reference: http://www.ibm.com/developerworks/aix/library/au-threadingpython/ I understand the logic in the code I just don't want to write a threading class for every method that I want threaded.
Here is the class that creates the context menu and then parses the json returned, I think I should add it to the for loop in the run command. Any help is greatly appreciated.
class SyncsnippetsCommand(sublime_plugin.TextCommand):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def buildLexerDict(self,snippets):
lexers = snippets[0]['user']['lexers']
lexer_dict = {}
for lexer in lexers:
lexer_dict[lexer] = []
return lexer_dict
def buildsnippetsContextDict(self,snippets,lexer_dict):
snippets_dict = lexer_dict
for snippet in snippets:
snippets_dict[snippet['lexer']].append({"id":str(snippet['id']),
"title":snippet['title']})
return snippets_dict
def run(self, edit):
snippet_url = buildsnippetURL()
snippets_count = 1;
snippets = getsnippets(snippet_url)
context_menu = '['
context_menu += '\n\t{ "caption": "snippets", "id": "file", "children":'
context_menu += '\n\t\t['
if snippets == None:
{"caption":"No snippets available"}
else:
snippets = snippets['objects']
lexers = self.buildLexerDict(snippets)
snippets_dict = self.buildsnippetsContextDict(snippets, lexers)
for j,key in reversed(list(enumerate(reversed(snippets_dict.keys())))):
... loop through JSON and create menu ...
if j == 0:
context_menu += ''
else:
context_menu += ','
context_menu += '\n\t\t]'
context_menu += '\n\t}'
context_menu += '\n]'
f = open(sublime.packages_path() + '\snippetSync\\Context.sublime-menu', 'w')
f.write(context_menu)
f.close
self.view.set_status('snippet', 'snippet Sync: Added ' + str(snippets_count) + ' snippets from your account.')
sublime.set_timeout(lambda: self.view.erase_status('snippet'), 3000)
return
Here is a simple Sublime Text 2 plugin with threading. What it does is insert Hello World! after 3 seconds. What you'll notice is that you can still move the cursor during those three seconds.
In your case, it looks like you just need to grab a bunch of snippets from an API and create a context menu from the returned data. Then there will be a notification at the bottom telling you how many snippets were added. I could be wrong, but you should be able to modify this code to make your plugin work.
import threading
import time
import sublime
import sublime_plugin
"""
The command just creates and runs a thread.
The thread will do all the work in the background.
Note that in your Thread constructor, you will need to pass in an
instance of your Command class to work with in your thread.
"""
class ExampleCommand(sublime_plugin.TextCommand):
def run(self, edit):
exampleThread = ExampleThread(self, edit)
exampleThread.start()
"""
Extend the Thread class and add your functionality in
the run method below.
One thing to remember when moving your code over is
you need to use self.cmd instead of self.
"""
class ExampleThread(threading.Thread):
"""
Remember to pass in the parameters you need
in this thread constructor.
"""
def __init__(self, cmd, edit):
threading.Thread.__init__(self)
self.cmd = cmd
self.edit = edit
"""
Add your functionality here.
If you need to access the main thread, you need to
use sublime.set_timeout(self.callback, 1).
In my example here, you can't call insert text into the editor
unless you are in the main thread.
Luckily that is fast operation.
Basically, time.sleep(3) is a slow operation and will block, hence it
is run in this separate thread.
"""
def run(self):
time.sleep(3)
sublime.set_timeout(self.callback, 1)
"""
This is the callback function that will be called to
insert HelloWorld.
You will probably need to use this to set your status message at
the end. I'm pretty sure that requires that you be on main thread
to work.
"""
def callback(self):
self.cmd.view.insert(self.edit, 0, "Hello, World!")
Update
I found some time integrate your code snippet above using the approach I outlined above. You'll still need to fill in some blanks, but hopefully this gives you an idea of where to put your code. I tested that the basic skeleton still works, which is why the section where you are building the context menu is commented out in this example.
import threading
import time
import sublime
import sublime_plugin
def buildsnippetURL():
return ""
def getsnippets(snippet_url):
time.sleep(3)
return ""
class SyncsnippetsCommand(sublime_plugin.TextCommand):
def run(self, edit):
syncsnippetsThread = SyncsnippetsThread(self, edit)
syncsnippetsThread.start()
class SyncsnippetsThread(threading.Thread):
def __init__(self, cmd, edit):
threading.Thread.__init__(self)
self.cmd = cmd
self.edit = edit
def buildLexerDict(self,snippets):
lexers = snippets[0]['user']['lexers']
lexer_dict = {}
for lexer in lexers:
lexer_dict[lexer] = []
return lexer_dict
def buildsnippetsContextDict(self,snippets,lexer_dict):
snippets_dict = lexer_dict
for snippet in snippets:
snippets_dict[snippet['lexer']].append({"id":str(snippet['id']),
"title":snippet['title']})
return snippets_dict
def run(self):
snippet_url = buildsnippetURL()
snippets_count = 1;
snippets = getsnippets(snippet_url)
"""
context_menu = '['
context_menu += '\n\t{ "caption": "snippets", "id": "file", "children":'
context_menu += '\n\t\t['
if snippets == None:
{"caption":"No snippets available"}
else:
snippets = snippets['objects']
lexers = self.buildLexerDict(snippets)
snippets_dict = self.buildsnippetsContextDict(snippets, lexers)
for j,key in reversed(list(enumerate(reversed(snippets_dict.keys())))):
... loop through JSON and create menu ...
if j == 0:
context_menu += ''
else:
context_menu += ','
context_menu += '\n\t\t]'
context_menu += '\n\t}'
context_menu += '\n]'
f = open(sublime.packages_path() + '\snippetSync\\Context.sublime-menu', 'w')
f.write(context_menu)
f.close
"""
sublime.set_timeout(lambda: self.callback(snippets_count), 1)
def callback(self, snippets_count):
self.cmd.view.set_status('snippet', 'snippet Sync: Added ' + str(snippets_count) + ' snippets from your account.')
sublime.set_timeout(lambda: self.cmd.view.erase_status('snippet'), 3000)

Categories

Resources