#πŸ”’ Creating a context menu in PySide6

609 messages Β· Page 1 of 1 (latest)

sleek parcel
#

Top secret mission to develop a context menu using pyside6

vague waspBOT
#

@sleek parcel

Python help channel opened

Remember to:

  • Ask your Python question, not if you can ask or if there's an expert who can help.
  • Show a code sample as text (rather than a screenshot) and the error message, if you've got one.
  • Explain what you expect to happen and what actually happens.

:warning: Do not pip install anything that isn't related to your question, especially if asked to over DMs.

sleek parcel
#

@dull inlet

#

so basically

hexed cosmos
dull inlet
#

I thought we're switching to tree widget?

sleek parcel
#

i mean we can do

#

i tried having a look at it earlier in college

#

but it confused me

dull inlet
#

ok, let's start from a blank file

#

make a class for your main window and add a layout and tree widget

sleek parcel
#

all my work? gone πŸ₯Ή

dull inlet
#

that's how you practice πŸ™‚

#

Surely you can make a class for a simple window by now, right?

hexed cosmos
sleek parcel
#

i mean its pretty structured right now

#

no problems

dull inlet
sleek parcel
#

so will we come back to this current file at any point

#

since its my github repo

dull inlet
#

you can keep it for later, sure

sleek parcel
#

okay

hexed cosmos
dull inlet
#

@sleek parcel do you think you can write a class for a basic window yet without looking at your old code?

#

I'll brb in a few minutes actually while you do that

sleek parcel
dull inlet
#

ok I'm back

sleek parcel
#

eh i got a little bit done

#
import sys
from PySide6 import QtWidgets, QtGui, QtCore

class MainWindow(QtWidgets.QMainWindow):

    DEFAULT_SIZE = (500, 500)

    def __init__(self):
        pass

window = MainWindow()
app = window.show()
#

thats as much as i can remember

dull inlet
#

That's pretty good but needs a few corrections

#

We need to create our application

#
app = QtWidgets.QApplication()
#

you can put sys.argv in there but I never bother

#

To run things, it's pretty much always

Create application
Create widget you want to display
Show widget
Start application

#
app = QtWidgets.QApplication()
window = MainWindow()
window.show()
app.exec()
sleek parcel
#

okay

dull inlet
#

We're also missing the super() call inside init, and we're going to be using QDialog instead of QMainWindow

#

but everything else is good πŸ™‚

sleek parcel
#
from PySide6 import QtWidgets, QtGui, QtCore

class MainWindow(QtWidgets.QDiaglog):

    DEFAULT_SIZE = (500, 500)

    def __init__(self):
        super().__init__()

app = QtWidgets.QApplication()
window = MainWindow()
window.show()
app.exec()
dull inlet
#

Dialog, not Diaglog

#

Always run to test

#

from PySide6.QtCore import Qt

#

I also include this import

#

since we'll need it for some things later

sleek parcel
#

okay done

dull inlet
#

Also just to make sure you know what we're building, we're going to make something like this

sleek parcel
#

okay cool

dull inlet
#

before we were working with a QListWidget

#

which is a container widget that displays QListWidgetItems

#

Now we're going to be making a QTreeWidget

#

want to guess what it displays?

sleek parcel
#

QTreeitems

#

this is what confused me

dull inlet
#

QTreeWidgetItem

sleek parcel
#

i didnt understand what QTreeWidgetView was

#

or something like that

dull inlet
#

don't worry about view

#

Widgets and Views are somewhat different

#

QTreeWidget and QTreeView work differently

sleek parcel
#

okay i see

dull inlet
#

View took me a lot longer to wrap my head around

#

but Widget isn't too bad

#

there's Widget/View for List, Tree, and Table

sleek parcel
#

how do i set the window size again

dull inlet
#

self.resize

#

or self.setFixedSize if you don't want to be able to change it after (but I think we just want resize here)

sleek parcel
#

its saying my default size doesnt exist

dull inlet
#

it would be MainWindow.DEFAULT_SIZE

#

Ok, so I want you to see an example of a more complex QTreeWidget, since it will explain some of the behaviours we have to work around

#

QListWidget is only ever a single column

#

but QTreeWidget can have multiple columns

#

Here's an example of one I have open from work (I didn't make this one though)

#

It displays parts of a 3d character skeleton

#

so we have 2 new features in QTreeWidget

sleek parcel
#

3 columns

dull inlet
#

we can have nested items

#

and we have columns

#

We still only really care about have 1 column

#

but we want the nested items

#

Unfortunately though, whenever we do something with the tree widget, we have to tell it what column we're working with

#

so it's just an extra thing to give it "column 0" for most methods

sleek parcel
#

we could have 2 columns, another saying if the task is completed or not

dull inlet
#

That's a good idea! We can start with 1 for now with the checkbox, and add that later if we want

hexed cosmos
# dull inlet

sure we could but isnt that what the radio boxes to the left of task here are for ? pithink

dull inlet
#

Ok, let's just make our tree so we can play around with it

sleek parcel
#

okay so what now

dull inlet
#

We don't have a layout yet, so we want to start with that

#

main_layout = QtWidgets.QVBoxLayout(self)

#

since this is our top-most layout, we can just use self in the argument

sleek parcel
#

why do i need to pass self into that

dull inlet
#

self is our window

sleek parcel
#

okay

dull inlet
#

and layouts can accept a parameter for its parent

sleek parcel
#

oh i see

dull inlet
#

we don't usually parent layouts this way

#

except for the top-most one

#

all other layouts, we would use addLayout

#

ok, now we have to create our tree widget, and add it to the layout

#

this process is basically identical for ANY widget

sleek parcel
#

let me try

dull inlet
#

Make widget
Add widget to some sort of layout

#

I called mine self.todo_wdg

sleek parcel
#
class MainWindow(QtWidgets.QDialog):

    DEFAULT_SIZE = (300, 325)

    def __init__(self):
        super().__init__()

        self.resize(*MainWindow.DEFAULT_SIZE)
        self.setWindowTitle("Todo List")

        main_layout = QtWidgets.QVboxLayout(self)

        self.tree_list_widget = QtWidgets.QTreeWidget()
        
        main_layout.add(self.tree_list_widget)
dull inlet
#

QVBox

#

not QVbox

sleek parcel
dull inlet
#

Name your widgets about what they represent, not what kind of widget they are

#

adding a widget is with addWidget, not add

#

they're all addSomething

sleek parcel
#

i also fixed that

dull inlet
#

addWidget
addLayout
addSpacing
addStretch

sleek parcel
#

now it just shows me a line with 1 in the column

dull inlet
#

Yup, by default, tree widgets have 1 column, and it's labeled 1

#

You can decide if we want to rename the header, or just remove it

sleek parcel
#

I might keep the header but rename it

dull inlet
#

We can rename it Tasks if you like

sleek parcel
#

i renamed it to that

#

and it works

#

setHeaderLabel

dull inlet
#

perfect πŸ™‚

#

that works for a single column

#

we can use a list of headers if we have multiple columns

#

but then we use setHeaderLabels

#

Ok, so before, working with QListWidget, you would create a QListWidgetItem and add it to the QListWidget

sleek parcel
#

yep

dull inlet
#

tree is slightly different

#

When we make a QTreeWidgetItem, we have a choice of where to add it

#

we can add it directly to the widget

#

or we can add it as a child of another item

sleek parcel
#

tasks wanna be directly to list

#

but subtasks wanna be a child

dull inlet
#

exactly πŸ™‚

#

Also when we create an item, we have to give it a list instead instead of a string. The list represents the columns

#

Since we're only working with 1 column, we'll always give it a list of 1 string

#

a bit annoying, but that's how it works

#

let's make an example item

#

then we can refactor it into a method

sleek parcel
#

okay ill try first

#
    def __init__(self):
        super().__init__()

        self.resize(*MainWindow.DEFAULT_SIZE)
        self.setWindowTitle("Todo List")

        main_layout = QtWidgets.QVBoxLayout(self)

        self.todo_wgt = QtWidgets.QTreeWidget()
        self.todo_wgt.setHeaderLabel("Tasks")

        top_task = QtWidgets.QTreeWidgetItem(["Quadratics Homework"])
        self.todo_wgt.addTopLevelItem(top_task)
        
        main_layout.addWidget(self.todo_wgt)
#

it looks a bit ugly but works

dull inlet
#

yeah perfect

sleek parcel
#

why do they stay permanently highlighted when i click them

#

so annoying

dull inlet
#

because you only have 1 item

hexed cosmos
dull inlet
#

yeah, unfortunately you can't really "click off" an item in these widgets

#

(without extra logic)

sleek parcel
dull inlet
#

ok, now try and add another item, but this time as a child in the Quadratics Homework

sleek parcel
#

will do

#
class MainWindow(QtWidgets.QDialog):

    DEFAULT_SIZE = (300, 325)

    def __init__(self):
        super().__init__()

        self.resize(*MainWindow.DEFAULT_SIZE)
        self.setWindowTitle("Todo List")

        main_layout = QtWidgets.QVBoxLayout(self)

        self.todo_wgt = QtWidgets.QTreeWidget()
        self.todo_wgt.setHeaderLabel("Tasks")

        top_task = QtWidgets.QTreeWidgetItem(["Quadratics Homework"])
        self.todo_wgt.addTopLevelItem(top_task)

        sub_task = QtWidgets.QTreeWidgetItem(["Complete the square"])
        top_task.addChild(sub_task)
        
        main_layout.addWidget(self.todo_wgt)
#

done

#

looks good now

dull inlet
#

See, not so bad?

sleek parcel
#

or better atleast

sleek parcel
dull inlet
#

yeah, stay away from views for now

sleek parcel
#

i like how on VSC i can hover over methods or whatever and it tells me the params

dull inlet
#

even still I rarely use them, although I do know their benefits

sleek parcel
#

its helpful knowing what i need to pass in

dull inlet
#

yeah that's super helpful. I still use it all the time

#

Imagine we had a list of data (like a python list), and every time it changed, a widget would also update based on that

#

that's more similar to what a view is

sleek parcel
#

also, i think i want a different method of adding tasks this time

dull inlet
#

instead of a line edit?

sleek parcel
#

yeah i think it would be a nice difference

dull inlet
#

What did you have in mind?

sleek parcel
#

you can right click to bring up a context menu

#

and you can add tasks, delete tasks, edit tasks

dull inlet
#

ahh ok, perfect

sleek parcel
#

and add subtasks if you rightclick over an existing task

#

i just think it could look cleaner

dull inlet
#

I agree

sleek parcel
#

and more compact

#

and its another tool that ive messed around with

dull inlet
#

Most widgets actually have right-click menus built in

#

but they're disabled by default

sleek parcel
#

oh awesome

dull inlet
#

we just need to re-enable it

sleek parcel
#

okay lets start with adding main tasks to the QTreeWidget?

hexed cosmos
dull inlet
sleek parcel
#

there we go fashoomp sent it

#

except his subtask wasn’t as cool as mine

hexed cosmos
# dull inlet

are u following every change he makes/sends locally ? pithink

sleek parcel
#

β€˜complete the square’ 😎

dull inlet
#

I have todo_wdg, he has todo_wgt

#

πŸ™‚

sleek parcel
#

lol

dull inlet
#

Well let's add the right click menu, and from there we can add our options and methods

#

So get ready for a mouthful

#
self.todo_wdg.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
#

it's one line, but it's long

#

we're setting the ContextMenuPolicy of the widget

#

and we're setting it to CustomContextMenu

#

well, there's 2 ways we can do this

hexed cosmos
sleek parcel
#

why the hell is their a policy

dull inlet
#

policy is basically a fancy word for "setting"

sleek parcel
#

oh lol

dull inlet
#

there's different "settings" we could choose for how right-click works on our widget

sleek parcel
#

so its setting the context menu to a custom one

dull inlet
#

by default it's NoContextMenu

#

for example though, look if we use a QLineEdit

#

it has a DefaultContextMenu

#

pretty common in text entry widgets

sleek parcel
#

oh i see

#

nothing is meant to happen yet right

#

right clicking does nothing

dull inlet
#

correct

sleek parcel
#

cool

dull inlet
#

we've just told it "I want to show a menu when I right click"

sleek parcel
#

i assume it needs a signal

dull inlet
#

but we need to create a slot for it

dull inlet
sleek parcel
#

okay let me try this

dull inlet
#

this one is a bit tricky

sleek parcel
#

self.todo_wgt.customContextMenuRequested.connect()

#

i found that

dull inlet
#

oh yeah, that's it

hexed cosmos
dull inlet
#

self.todo_wdg.customContextMenuRequested.connect(self.on_right_click)

#

I'm connecting to a new method on_right_click

hexed cosmos
#

welp thats a darn good tactic

dull inlet
#
def on_right_click(self):
    print("Hurray!")
sleek parcel
dull inlet
#

I usually do something like this and test it to make sure it's already working as expected

sleek parcel
#

okay my console is succesfully printing yippee on right click

dull inlet
#

perfect

sleek parcel
#

so my next step issss to add something to the context menu

dull inlet
#

ok, so we actually need to create the menu

#

we want to create the menu first, and then display the menu on right click

#

if we wanted it a bit more dynamic, we could rebuild it each time on right click

sleek parcel
#

so right now its only able to be created

#

now we gotta create it

dull inlet
#

like if clicking subtasks was different from clicking top tasks

sleek parcel
#

i dont think it will be

#

they do the same thing pretty much

dull inlet
#

yeah ok, good

#

Let's actually create another method for creating our menu

#

I usually organize my class by widgets, layouts, connections, and menus

hexed cosmos
dull inlet
dull inlet
#

we can technically designate any widget to be a popup widget with a flag

hexed cosmos
#

which flag pithink

dull inlet
#

this is usually how I arrange things

sleek parcel
#

i think i do that

dull inlet
sleek parcel
#
class MainWindow(QtWidgets.QDialog):

    DEFAULT_SIZE = (300, 325)

    def __init__(self):
        super().__init__()

        self.resize(*MainWindow.DEFAULT_SIZE)
        self.setWindowTitle("Todo List")

        main_layout = QtWidgets.QVBoxLayout(self)

        self.todo_wgt = QtWidgets.QTreeWidget()
        self.todo_wgt.setHeaderLabel("Tasks")
        self.todo_wgt.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        
        main_layout.addWidget(self.todo_wgt)

        self.todo_wgt.customContextMenuRequested.connect(self.on_right_click)

    def on_right_click(self):
        print("yippee")

    def create_menu(self):
dull inlet
#

I just use this and inherit from it whenever I want a custom popup tool

hexed cosmos
#

its literally called Qt.Popup got it pithink

dull inlet
sleek parcel
#

alright

dull inlet
#

self.right_click_menu = QtWidgets.QMenu(self)

#

we're also giving it self as parent so it knows who the menu belongs to

#

if we plan on attaching this to something like a menu bar, it's less important

#

ok so for each action, we do this in 2 steps. First we add the action, then we connects its signal

sleek parcel
#

okay

dull inlet
#
add_task_action = QtGui.QAction("New Task...")
add_task_action.triggered.connect(self.on_add_task_triggered)
#

We'll add it to the menu in a moment

sleek parcel
#

all in the create menu method right?

dull inlet
#

Yes

sleek parcel
#

cool

dull inlet
#

I create all my actions/connections first

#

then I add them to the menu later

sleek parcel
#

yep thats fine

dull inlet
#

that makes it a bit easier if I want to adjust the order they appear

sleek parcel
#

yeah true

#

should i create the other 2

#

edit and delete

#

or wait until later

dull inlet
#

Sure, you can

sleek parcel
#

okay done that

dull inlet
#

Ok, make sure you make the methods as well, even if they just have pass for now

sleek parcel
#

yes i have

dull inlet
#

perfect

#

there's one thing I realized we're missing

#
add_task_action = QtGui.QAction("New Task...", self)
#

we need self as an argument for the action

#

Well, either we need this, or we need add_task_action to be an attribute

#

self.add_task_action

#

as you might know, objects without references to them are destroyed

#

and local variables no longer exist after a function is called

#

so we need some way to preserve the actions otherwise they get removed

#

technically we could create this "add task action" and add it to many different menus. Actions are really useful that way

#

anyways, just add self like I have above and we can move on πŸ™‚

#

Inside the on_right_click method, now we just need to show the menu

#

Oh, and make sure you're calling the create_menu method from __init__

dull inlet
#

we made a method for creating the menu, but the menu won't be created unless we call that method

#
def __init__(self):
    super().__init__()

    main_layout = QtWidgets.QVBoxLayout(self)

    self.todo_wdg = QtWidgets.QTreeWidget()
    self.todo_wdg.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
    self.todo_wdg.customContextMenuRequested.connect(self.on_right_click)

    self.todo_wdg.setHeaderLabel('Tasks')
    main_layout.addWidget(self.todo_wdg)

    top_task = QtWidgets.QTreeWidgetItem(["Quadratics Homework"])
    self.todo_wdg.addTopLevelItem(top_task)

    child_task = QtWidgets.QTreeWidgetItem(["10 Exercises"])
    top_task.addChild(child_task)

    self.create_menu()
#

this is my __init__ right now

sleek parcel
#

oh okay

dull inlet
#

to display our menu, it's simply self.right_click_menu.exec()

#

but, we need an argument

#

well, try it first without

sleek parcel
#

do we need something inside right_click method

#

oh wait

dull inlet
#

yes, that's where we're doing exec on our menu

sleek parcel
#

nothing is popping up

dull inlet
#

look in the top left of your screen

#

and try again

hexed cosmos
dull inlet
#

My last message is a hint

sleek parcel
#

thats it

dull inlet
sleek parcel
#

it looks the same

dull inlet
#

show your latest code?

sleek parcel
#
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Qt

class MainWindow(QtWidgets.QDialog):

    DEFAULT_SIZE = (300, 325)

    def __init__(self):
        super().__init__()

        self.resize(*MainWindow.DEFAULT_SIZE)
        self.setWindowTitle("Todo List")

        main_layout = QtWidgets.QVBoxLayout(self)

        self.todo_wgt = QtWidgets.QTreeWidget()
        self.todo_wgt.setHeaderLabel("Tasks")
        self.todo_wgt.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        
        main_layout.addWidget(self.todo_wgt)

        self.create_menu()

        self.todo_wgt.customContextMenuRequested.connect(self.on_right_click)

    def on_right_click(self):
        self.right_click_menu.exec()

    def create_menu(self):
        self.right_click_menu = QtWidgets.QMenu(self)

        add_task_action = QtGui.QAction("New Task...", self)
        add_task_action.triggered.connect(self.on_add_task_triggered)

        edit_task_action = QtGui.QAction("Edit Task", self)
        edit_task_action.triggered.connect(self.on_edit_task_triggered)

        delete_task_action = QtGui.QAction("Delete Task", self)
        delete_task_action.triggered.connect(self.on_delete_task_triggered)

    def on_add_task_triggered(self):
        pass

    def on_edit_task_triggered(self):
        pass

    def on_delete_task_triggered(self):
        pass

app = QtWidgets.QApplication()
window = MainWindow()
window.show()
app.exec()
dull inlet
#

ahh right we never added our actions to the menu

sleek parcel
#

ohh lol

dull inlet
#
self.right_click_menu.addAction(add_task_action)
#

I would do this below where you create all your actions though

#

they will appear in the order you have them added

hexed cosmos
dull inlet
#

Here's a recent example from my code

sleek parcel
#

okay so we need to create the menu over my cursor

#

not the top left

dull inlet
#

you can see how I have them at the bottom, which makes it easy to rearrange

sleek parcel
#

also would you suggest any way i should clean up my code

#

or reorganise things?

dull inlet
#

and same for layouts and connections

sleek parcel
#

how would i do that

dull inlet
#

so your init basically just looks like

dull inlet
#

here's a recent example from mine

#
class AssetSelectWindow(ui_utils.MayaWindow):

    PACKAGE_ROLE = Qt.UserRole
    icon_size = QtCore.QSize(100, 100)
    icon_scale = 1.2

    def __init__(self):
        super().__init__()
        self.setWindowTitle("Select your fighter!")

        self.create_widgets()
        self.create_layouts()
        self.create_connections()

        if os.path.exists(RIG_MANIFEST):
            rig_packages = get_packages_from_manifest()
        else:
            rig_packages = get_all_rig_packages()

        self.add_packages_to_css(rig_packages)
        self.resize(750, 550)
sleek parcel
#

okay ill try

dull inlet
#

__init__ is nice and organized. Everything exciting is going on in those 3 other methods

sleek parcel
#

thats cool

#

does connections contain the signals and slots

dull inlet
#

just the signals

#

the slots are always their own methods

sleek parcel
#

okay

dull inlet
#
def create_connections(self):
    self.search_bar.textChanged.connect(self.proxy_model.setFilterRegExp)
    self.toggle_grid_mode_btn.toggled.connect(self.toggle_grid_mode)
    self.refresh_rig_list_btn.clicked.connect(self.reload_rig_manifest)
    self.missing_ref_btn.clicked.connect(self.on_missing_ref_btn_clicked)
    self.repair_textures_btn.clicked.connect(self.on_repair_textures_btn_clicked)
hexed cosmos
dull inlet
hexed cosmos
sleek parcel
#
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Qt

class MainWindow(QtWidgets.QDialog):

    DEFAULT_SIZE = (300, 325)

    def __init__(self):
        super().__init__()

        self.resize(*MainWindow.DEFAULT_SIZE)
        self.setWindowTitle("Todo List")

        self.create_widgets()
        self.create_layout()
        self.create_connections()
        self.create_menu()

    def create_widgets(self):
        self.todo_wgt = QtWidgets.QTreeWidget()
        self.todo_wgt.setHeaderLabel("Tasks")
        self.todo_wgt.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)

    def create_layout(self):
        main_layout = QtWidgets.QVBoxLayout(self)
        main_layout.addWidget(self.todo_wgt)

    def create_connections(self):
        self.todo_wgt.customContextMenuRequested.connect(self.on_right_click)

    def create_menu(self):
        self.right_click_menu = QtWidgets.QMenu(self)

        add_task_action = QtGui.QAction("New Task...", self)
        add_task_action.triggered.connect(self.on_add_task_triggered)

        edit_task_action = QtGui.QAction("Edit Task", self)
        edit_task_action.triggered.connect(self.on_edit_task_triggered)

        delete_task_action = QtGui.QAction("Delete Task", self)
        delete_task_action.triggered.connect(self.on_delete_task_triggered)

        self.right_click_menu.addAction(add_task_action)
        self.right_click_menu.addAction(edit_task_action)
        self.right_click_menu.addAction(delete_task_action)

    def on_right_click(self):
        self.right_click_menu.exec()

    def on_add_task_triggered(self):
        pass

    def on_edit_task_triggered(self):
        pass

    def on_delete_task_triggered(self):
        pass

app = QtWidgets.QApplication()
window = MainWindow()
window.show()
app.exec()
#

thats my entire code now

dull inlet
#

like one UI might be 7 different classes

sleek parcel
#

awesome

dull inlet
#

ok, so for exec, we need to popup the menu at our mouse cursor

#

we can get our mouse position quite easily in pyside

#

QtGui.QCursor().pos()

sleek parcel
#

okay

dull inlet
#

So what should happen when you click New Task?

#

You can make the items in your tree editable

sleek parcel
dull inlet
#

or you can make a popup window you can type into

dull inlet
sleek parcel
#

okay

sleek parcel
#

well

#

you should be prompted to enter a task name

#

then when enter is pressed it adds it

dull inlet
#

Ok, so like a little dialog box?

sleek parcel
#

maybe yeah

#

idk what dialog actually means

dull inlet
#

dialog is just a window

#

something like this

#

but with a lineedit inside it

sleek parcel
#

Yes like that

dull inlet
hexed cosmos
sleek parcel
#

Yes looks good

hexed cosmos
#

no matter how many times i think "surely there cant be another term im not familiar with in gui" i find like 3

#

then i find another 3 i already knew meant different things

dull inlet
#

that's how I learned about a lot of new widgets

dull inlet
sleek parcel
#

oh awesome lets do it

dull inlet
#

but there's something we need to consider

#

is the new task a top task or a sub task?

sleek parcel
#

well it depends what you right clicked on

dull inlet
#

ok, so we need to know what we have selected when the right click menu appears

sleek parcel
#

yes

dull inlet
#

I would recommend adding your example Quadratic Homework task back in

#

just to make it easier to test things

sleek parcel
#

okay let me try again

dull inlet
#

I almost always have sample data in my GUI when I'm working on it

sleek parcel
#

whys it only displaying the first character of the tasks label

dull inlet
#

Remember your QTreeWidgetItem must be a list (or rather, any iterable)

#

if you give it a string, it treats each character of the string as a different column

sleek parcel
#

oh yeah

dull inlet
#

I make that mistake all the time too

sleek parcel
#

okay sorted

dull inlet
#

so, now we're going to work inside the on_add_task_triggered method

#

because that's where we need to decide what will happen when that action is selected from the menu

#

Let's first figure out what we have selected

sleek parcel
#

yes

dull inlet
#
selected = self.todo_wgt.selectedItems()
#

exact same as QListWidget

sleek parcel
#

edit and delete shouldnt matter whats selected

dull inlet
#

shouldn't edit matter?

#

and delete?

#

how do you know what to delete?

sleek parcel
#

oh i guess it matters whats selected

#

i just assumed that right clicking over whatever it is, is the one targetted

dull inlet
#

No, we have to get our selection once the action item is triggered

#

Unless we want it so right clicking can't modify selection

#

like you make your selection and then right click

#

this is UX design in a nutshell

#

you have to work out the user experience

#

how exactly do they interact with your UI

sleek parcel
#

okay i see

#

I made the selection var

dull inlet
#

try printing it out so you can see what it is

sleek parcel
#

i get a memory location of an object

dull inlet
#

it's a list of QTreeWidgetItems

#

even though we can only select 1 thing at a time, it's still a list

#

try launching it again and right click > add task before selecting anything

sleek parcel
#

empty list

dull inlet
#

exactly

#

so, we need to decide the scenario that leads to adding a top task, and what scenario leads to adding a child task

#

I think "no selection" should obviously be "new top task"

sleek parcel
#

so if selected == []

#

make new top task

dull inlet
#

we can just say if not selected

sleek parcel
#

oh does [] mean theres nothing there

#

i thought it would still mean selected is something

dull inlet
#

yes it's the same thing, but the "pythonic" way would be if not selected

sleek parcel
#

okay i see

dull inlet
#

let's make a new method for adding a task

#
def add_new_top_task(self, task_name):
sleek parcel
#

should i put this between my add and edit selected methods

dull inlet
#

it doesn't really matter

#

you can organize it if you want

#

I don't really organize my methods too much

#

so you pretty much already have the logic for this

sleek parcel
#

lol okay

dull inlet
#

since you made your example tasks

#

I'll brb in a few minutes, see if you can figure it out

sleek parcel
#

yes let me try

dull inlet
#

Remember it's just

Make new qtreewidgetitem
Add qtreewidgetitem to the qtreewidget

sleek parcel
#

i did that

#

now i need the dialog

#
def add_new_top_task(self, item):
        self.new_top_task = QtWidgets.QTreeWidgetItem([item.text()])
        self.todo_wgt.addTopLevelItem(self.new_top_task)
dull inlet
#

I don't know where you got item.text() from

#

we simply want to give it the string of the new task's name

sleek parcel
#

i used it in my last todo list

dull inlet
#
def add_new_top_task(self, task_name):
    top_task = QtWidgets.QTreeWidgetItem([task_name])
#

we just want to pass the parameter directly into it

sleek parcel
#

okay

dull inlet
#

the reason we're making this its own method is imagine we're reading from a text file instead

#

we want a nice method to be able to create our items

#

we don't care where the text is coming from

#

we just want to say "add a new task with this name"

sleek parcel
#

i mean i probably need a text file or something at some point to save tasks over uses

#

probably json

dull inlet
#

this is also where we can put any item styling

#

so for example, we can add our checkbox from here

sleek parcel
#

oh yes

dull inlet
#

This is an example of where we need to give the column name to the item

#
top_task.setCheckState(0, Qt.CheckState.Unchecked)
#

so it's nearly the same as list widget, but we have to tell it column 0 should have a checkbox

sleek parcel
#

inside where do i put this?

dull inlet
#

still inside the add_new_top_task method

sleek parcel
#

okay

dull inlet
#
def add_new_top_task(self, task_name):
    top_task = QtWidgets.QTreeWidgetItem([task_name])
    top_task.setCheckState(0, Qt.CheckState.Unchecked)
    self.todo_wgt.addTopLevelItem(top_task)
    return top_task
#

This is what I have. We want to make sure to return the new item as well

#

that way we can access it later on after adding it

sleek parcel
#

okay cool ive done that

dull inlet
#

Ok back to the "on_add_task_triggered" method

sleek parcel
#

alright

dull inlet
#

we can start to test this by calling the method

sleek parcel
#

we need to call the method

dull inlet
#
def on_add_task_triggered(self):
    selected = self.todo_wgt.selectedItems()
    if not selected:
        self.add_new_top_task("Example")
sleek parcel
#

infact i did it when i made the method

dull inlet
#

This is a good way to test that your methods are already hooked up correctly

#

the first task wasn't added using the new method we just created, so we can update that too

sleek parcel
#

yesss

dull inlet
#

but we can wait until we have the subtask method too

sleek parcel
#

it stops working once you select a task

dull inlet
#

Do you want subtasks to be able to have subtasks?

sleek parcel
#

but thats fine

dull inlet
sleek parcel
#

i dont want limits

dull inlet
#

ok good, so we don't even have to limit it

sleek parcel
#

i actually wanna use this program potentially so

#

i wanna tailor it perfectly how i want it

dull inlet
#

So there's a pretty simple way to work out if something is a child or not

#

we can get selected[0].parent()

#

remember selected is a list

#

if it's a top level item, parent() will be None

#

otherwise, it will be another QTreeWidgetItem

sleek parcel
#

im confused why it gave me a memory location earlier

dull inlet
#

that's what instances of any widget looks like

#

!e

class Foo:
    pass


print(Foo())
vague waspBOT
sleek parcel
#

oh okay

dull inlet
#

What did you expect it to look like?

sleek parcel
#

uhhh idk

#

a list with the subtask in

dull inlet
#
<PySide6.QtWidgets.QTreeWidgetItem object at 0x000001C89FC92140>
#

get used to seeing instances like this

#

all we need to know is that it's a QTreeWidgetItem

sleek parcel
#

okay

dull inlet
#

one sec I'm going to reset my PC real quick, it's acting up

#

we need a method for adding a sub item though

#

we basically have the logic for that too written above when you add your example task

#

see if you can work it out while I reset

sleek parcel
#

okay will do

dull inlet
#

Pc is updating 😬

sleek parcel
#

lol

#

idk how to get the parent of the subtask

dull inlet
#

Don't worry about that yet

#

The method just needs it as a parameter

sleek parcel
#

but how do i use the param to add the subtask too it

hexed cosmos
dull inlet
sleek parcel
#

yeah idk what to do

#

i gave parent param

#

thats it

dull inlet
sleek parcel
#
ef add_new_sub_task(self, task_name, parent):
        new_sub_task = QtWidgets.QTreeWidgetItem([task_name])
        new_sub_task.setCheckState(0, Qt.CheckState.Unchecked)
sleek parcel
#

i cant do

#

self.parent.addChild

#

so what do i do

dull inlet
#

you just do parent.addChild

#

that's the parameter

#

I wouldn't even call the parameter parent

#

just call it item

#

item.addChild(new_sub_task)

sleek parcel
#

so how do i pass in parent

dull inlet
#

we'll work that out now

#

the method doesn't care though

#

we want to keep methods as simple and generic as possible

#

it simply says "give me an item, and I will create a new item and make it a child of that item"

#

again, we don't care how it knows where the item is coming from

#

it could be from a click

#

it could be randomly selected

#

it could be "2 items down"

#

that's only for us to worry about when we call the method, not when we write the method

sleek parcel
#

okay

dull inlet
#

What does your sub item method look like now?

sleek parcel
#
def add_new_sub_task(self, task_name, parent):
        new_sub_task = QtWidgets.QTreeWidgetItem([task_name])
        new_sub_task.setCheckState(0, Qt.CheckState.Unchecked)
        parent.addChild(new_sub_task)
        return new_sub_task
dull inlet
#

perfect

#

Ok so when we right click and add task

#

it's a top level task if we have no selection, or if our item has no parent

#

wait hang on

#

if we're selecting a top level item, we probably want our new task to be a child of that item

sleek parcel
#

yes

dull inlet
#

how do you want to add new top level tasks then?

sleek parcel
#

it works right now no?

dull inlet
#

only when you first launch and have no selection

#

but once you select something, you can't deselect

sleek parcel
#

oh

#

erm

dull inlet
#

ok we can change this just a bit

#

instead of selectedItems

#

we can use itemAt

#

to get the widget at our cursor

sleek parcel
#

okay done

dull inlet
#

just a sec, I need to work this out, haha

sleek parcel
#

its okay lol

dull inlet
#

still learning about qtree as well πŸ™‚

sleek parcel
#

its fine lol i still wanna cry reading a thousand different self. attributes

dull inlet
#

you get used to it

#

the crying, that is

sleek parcel
#

lmao :-;

dull inlet
#

ahh ok I see the issue

#

I'm dumb

sleek parcel
#

what am i then

#

no joking

dull inlet
#

I was getting the position of the mouse when I clicked the menu, not when I clicked the item

sleek parcel
#

also i just wanna say im very appreciative of the help youve done

#

wouldnt of learnt this without you

dull inlet
#

Happy to help! I learn a lot from it as well

#

There's a few different ways to go about solving this and I'm trying to think of what would be the least painful

sleek parcel
#

ah damn it man

dull inlet
#

I think our best bet is to store the mouse position when we bring up the right click menu

sleek parcel
#

ive gotta go for dinner

#

sorry man hate to leave it right in the middle of this

dull inlet
#

Your homework is to open a blank file and rebuild this again

sleek parcel
#

but ive definitely learnt alot today

dull inlet
#

and again

#

and again

dull inlet
#

excellent

#

neat

vague waspBOT
#
Python help channel closed for inactivity

This help channel has been closed. Feel free to create a new post in #1035199133436354600. To maximize your chances of getting a response, check out this guide on asking good questions.