Drag and drop menuitems, menus on a menubar

Hello,

I aim aiming to make a favorites/bookmarks toolbar similar to firefox, chrome and other browsers. This is a toolbar where you can drag and drop links, create folders and org anise links within folders.

The functionality I would like to implement is:

  • Drag and drop URLs/files to the toolbar to create new links
  • Have folders on the toolbar
  • Drag and drop links/urls into a specific folder, by hovering over the folder, wait for it to open and drop the link into the shown list.
  • Links can be dragged and dropped to be reorganize within folder structure
  • Folders can have subfolders
  • Folders can be dragged and dropped to reorganize the folder structure

My attempts
I have been looking at the wx.Menubar, wx.Menu and wx.MenuItem. I have built a folder structure using menus and submenus. Where I have got stuck now is trying to implementing the drag and drop functionality.

Reading tutorials, I have implementing a wx.TextDropTarget class and then using SetDropTarget() method to add attach the class to a widget. I have done this successfully managed to implement drag and drop to a textbox and also to the menubar.

Where Iā€™m stuck
Where I run into problems is dragging onto the menubar I canā€™t figure out how to make the menu open when I hovering over it. The reason for this is later it will allow me to drag existing links (wx.menuitem), folders (wx.menu) to new locations to cover the reorganization I required (see above).

After Iā€™ve got text drag and drop I intend to look at dragging of menus and menuitems. If anyone knows if this is possible or not itā€™ll be much appreciated to get a heads up before I take on this next bit of the project.

Questions:

  • For the favorites toolbar functionality is the wx.Menubar a good route to go down?
  • Does anyone know how to drag and drop menuitems within a menu or have example code?
  • If the menu bar isnā€™t a good route, what other ideas, suggestions and directions could I look at?

Many thanks
Paul

Iā€™m sure that it is impossible for menu or menubars.

But for other controls like toolbars, itā€™s not hard to get files, text, etc. using DnD.
For example, the following code (slightly modified) from ā€œwx/py/shell.py:1548:ā€ would be helpful:

class TextAndFileDropTarget(wx.DropTarget):
    def __init__(self, target):
        wx.DropTarget.__init__(self)
        self.target = target
        self.compdo = wx.DataObjectComposite()
        self.textdo = wx.TextDataObject()
        self.filedo = wx.FileDataObject()
        self.compdo.Add(self.textdo)
        self.compdo.Add(self.filedo, True)
        self.SetDataObject(self.compdo)

    def OnData(self, x, y, result):
        self.GetData()
        if text := self.textdo.GetText():
            print(text)
            self.textdo.SetText('')
        elif filenames := self.filedo.GetFilenames():
            print(filenames)
        return result
# Usage:
>>> self.SetDropTarget(TextAndFileDropTarget(self))

I think some of them are possible by customizing the toolbar, but that would be quite many customizations :cry:

I think that TreeCtrl seems a little easier. :roll_eyes:

I believe you can use both the Menu and MenuBar. That said, I like the of adding a panel for adding URLā€™s. The Menu has ā€œ.appendMenuā€ and you can call update(). Just create a class (I call mine HaderToolBar) with icons, text buttons, etcā€¦ and in the class have the on_click use either the wx.Lib.agw.hyperlink or maybe wx.html.HtmlWindow with the URL you passed.

Johnf

Itā€™s not the kind of feature the OP said in the title, but I think itā€™s reasonable.

I canā€™t believe it unless someone performs this. :grimacing:
(I mean, ā€œDrag and drop menuitems, menus on a menubarā€)

I donā€™t have time to create some demo code. But I canā€™t see any reason that a menu item canā€™t be added dynamically via code. I create my menus by using menu.append. They are static but I have used many dynamic data sets to create other controls or widgets to create a form.
If I had a need to do what the OP wants I would store the data (menu items and submenus) in a database and the update the menu using the stored data. I do it for my reportlab reports daily where I dynamically add widgets depending on what the users wants the criteria for the report to be. And when the user wants to change the report criteria I just update the form with update() and it works and has for many years.
Johnf

Yes, it is possible to create a menu dynamically via code.
But, the OPā€™s request is DnD function on the menu like chrome.

K

youā€™ll need for each of the ā€˜itemsā€™ you envisage at least a wx.DataObject and one for whole folders is very ambitious (I donā€™t think that belongs into a general Gui-library) :astonished:

update on what Iā€™ve tried/researched:

  1. I trialed other gui libraries looking for a quick solution. I tried qt and tkinter neither had the functionality. Both have the same issue of not opening the menu when trying to drag and drop new data over the top.

  2. I looked into extending some of the menu functionality. I had the idea of using the drag and drop event to try and trigger a menu open with the following code, but it didnt work so I gave up on that avenue.

def OnDragOver(self, x, y, d)
    event = wx.MenuEvent(wx.wxEVT_LEFT_DOWN, menuitem.GetId(), menu)
    wx.PostEvent(frame, event)
  1. I have had a look into wxpython source code for the menus looking to add functionality. I compared the source to TreeCtrl which has drag and drop built in with tree expansion to drop an item into a spefic location within the tree. I found that wx.Tree Ctrl has Has 21 events where as wx.Menu has 4 events. The TreeCtrl has 2 specific drag events, EVT_TREE_END_DRAG and EVT_TREE_BEGIN_DRAG and some other events todo with expansion. When looking into the code it seamed very complex, so I thought I should move onto another avenue of investigation first.

  2. I have started looking at source code for web browsers. I have looked at KDE konqueror (which uses Qt). The bookmark bar seams to use a regular toolbar (not a menubar) and have some special buttons that act like menus when clicked. (source code konqbookmarkmenu.cpp)

Summary
I think looking into a regular toolbar and buttons that act like menus might be a good route to persue next rather than trying to add/modify existing functionality.

Questions

  • Are there button widgets that have popout menus attached to them wxpython?
  • Can wxpython do drag and drop with button widgets or button widgets with menu popouts?

In the wxPython demo application, under the Advanced Generic Widgets section there is a demo called ā€œAUIā€ (for Advanced User Interface).

If you select and run that demo, it should open an ā€œAUI Test Frameā€ window.

Under the menubar there are 2 toolbars. The second toolbar contains one item with a drop-down menu (Item 1) plus 7 items without drop-down menus, a separator and a choice control.

If you shrink the width of the window it should move the controls that no longer fit the toolbar into another drop-down menu, which can be opened via a small down-arrow button (however, when I press that button on linux, the drop-down doesnā€™t display correctly for some reason).

Perhaps you could adapt parts of the demo to do what you want?

See also: wx.lib.agw.aui.auibar.AuiToolBar ā€” wxPython Phoenix 4.2.0 documentation

I think the answers to both are yes. For example,

import wx

class MyDataLoader(wx.TextDropTarget):
    def __init__(self, target):
        super().__init__()
        self.target = target
    
    def OnDropText(self, x, y, data):
        self.target.OnContextMenu(url=data)
        return True

class TestPanel(wx.Panel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        btn = wx.Button(self, label="Drop here", pos=(10,10))
        btn.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu)
        btn.SetDropTarget(MyDataLoader(self))

    def OnContextMenu(self, evt=None, url=''):
        menu = wx.Menu()
        item = menu.Append(101, "Navigate...")
        self.Bind(wx.EVT_MENU, lambda v: self.Navigate(url), item)
        self.PopupMenu(menu)
        menu.Destroy()

    def Navigate(self, url):
        print(url)

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    panel = TestPanel(frm)
    frm.Show()
    app.MainLoop()

The next idea would be:

  1. Set the drop target of the button to a popup window instead of a menu.
  2. Lay out the list or tree control in the popup window, which has other drop targets.

I suspect that:

  1. The ā€œdrop-down menuā€ is a normal menu (not a popup window drawn by agw; it triggers menu events such as EVT_MENU_OPEN), and doesnā€™t have DnD interface.
  2. The ā€œsmall down-arrow buttonā€ may not be a button widget (doesnā€™t appear in the InspectionTool tree).

Tested with Windows 10.

I think @Paul_Macey isnā€™t talking of DnD across apps, rather of shuffling stuff around (move and drop into the right place) to reorganize (in modern versions of browsers it looks like DnD in the same app) :face_with_hand_over_mouth:
PS I forgot the snippet (to catch hold of the idea) :sweat_smile:

import wx

class Gui(wx.Frame):

    def __init__(self, parent):
        print(f'{self.__class__} __init__')
        super().__init__(parent, title='DnD for newbies')

        self.sizer = wx.WrapSizer()
        for n in range(20):
            self.sizer_add(self.get_btn(self, ''.join(('btn', str(n)))))
        self.SetSizer(self.sizer)
        self.Centre()
        self.Show()

    def evt_motion(self, evt):
        if evt.Dragging() and evt.LeftIsDown():
            obj = evt.GetEventObject()
            if 'src' in vars(self):
                x, y = obj.ClientToScreen(evt.GetPosition())
                self.cur.Move((x - self.delta[0], y - self.delta[1]))
            else:
                obj.SetBackgroundColour(wx.SystemSettings.GetColour(
                                                    wx.SYS_COLOUR_HIGHLIGHT))
                self.src = obj
                dlg = wx.Dialog(self,
                    pos=self.ClientToScreen(obj.GetPosition()),
                    size=obj.GetSize(),
                    style=wx.DEFAULT_FRAME_STYLE & ~\
                                    (wx.RESIZE_BORDER | wx.CAPTION))
                dlg.SetCursor(wx.Cursor(wx.CURSOR_HAND))
                btn = self.get_btn(dlg, obj.GetLabel())
                btn.Bind(wx.EVT_LEFT_UP, self.evt_left_up)
                dlg.SetTransparent(128)
                dlg.Show()
                self.delta = dlg.ClientToScreen(evt.GetPosition())\
                                                            - dlg.GetPosition()
                self.cur = dlg

    def evt_left_up(self, evt):
        if 'src' in vars(self):
            wx.SetCursor(wx.Cursor())
            x = wx.GetMouseState().GetX()
            y = wx.GetMouseState().GetY()
            objs = []
            for entry in self.sizer.GetChildren():
                obj = entry.GetWindow()
                objs.append(obj)
                if obj != self.cur:
                    rect = obj.GetScreenRect()
                    l = rect[0]
                    r = l + rect[2]
                    t = rect[1]
                    b = t + rect[3]
                    if l < x and x < r and t < y and y < b:
                        t_obj = obj
            if 't_obj' in vars():
                self.sizer.Clear()
                for entry in objs:
                    if entry == self.src:
                        self.sizer_add(t_obj)
                    elif entry == t_obj:
                        self.sizer_add(self.src)
                    else:
                        self.sizer_add(entry)
            else:
                wx.Bell()
            self.src.SetBackgroundColour(None)
            del self.src
            self.cur.HideWithEffect(wx.SHOW_EFFECT_EXPAND)
            self.cur.Destroy()
            self.Layout()
        evt.Skip()

    def get_btn(self, parent, lb):
        btn = wx.Button(parent, label=lb)
        btn.Bind(wx.EVT_MOTION, self.evt_motion)
        return btn

    def sizer_add(self, obj):
        self.sizer.Add(obj, 0, wx.LEFT|wx.TOP|wx.RIGHT, 5)

app = wx.App()
Gui(None)
app.MainLoop(
1 Like

Thank you for the snippet, DnD effect looks nice!

I have been thinking of DnD URL text from the Chrome browser and arranging it in the tree controls.
It would be even nice if the bookmark folder can be dropped, however, the data object is totally unknown.

Iā€™m pleased you like it (although one needs a Bitmap there), but the OP (@Paul_Macey) seems to have given up (though Iā€™m still befuddled of what he is after :thinking:)

I think Edge is doing it the way you think: it looks & feels (finally?) like the File Explorer and one can move the entries around by mouse (but donā€™t drop it in the wrong place: itā€™s hard to find it again :rofl:)

Hi All,

Thanks for the help and suggestions.

Iā€™ve recorded a quick video to help with visualizing the functionality Iā€™m trying to make.

The functionality Iā€™m trying to implement is:

  • Drag and drop URLs/files to the toolbar to create new links
  • Have folders on the toolbar
  • Drag and drop links/urls into a specific folder, by hovering over the folder, wait for it to open and drop the link into the shown list.
  • Links can be dragged and dropped to be reorganize within folder structure
  • Folders can have subfolders
  • Folders can be dragged and dropped to reorganize the folder structure

Update
Thanks to @RichardT for suggestiong the AUI toolbar library. Iā€™m currently investigating how to use that, Itā€™s got a lot of functions exposed and more events to hook into, so I think its much more likely that Iā€™ll be able to use this menu system instead.

Iā€™m currently trying to work out how to programaticly trigger a wx.lib.agw.flatmenu to open. I think plan to call this on a OnDragOver event to open the menu.

Does anyone know how to active ā€œopenā€ wx.lib.agw.flatmenu programmatically?

My work in progress
Iā€™m reviewing the source code here: /usr/lib/python3/dist-packages/wx/lib/agw/flatmenu.py

The functions Iā€™m trying to get working is this one for whick I need to pass in menuInfo which Iā€™m strugging to create at the moement. Iā€™m going to try menu id and see if thats enough.

def ActivateMenu(self, menuInfo):
    """
    Activates a menu.

    :param `menuInfo`: an instance of :class:`wx.MenuEntryInfo`.
    """

    # first make sure all other menus are not popedup
    if menuInfo.GetMenu().IsShown():
        return

    idx = wx.NOT_FOUND

    for item in self._items:
        item.GetMenu().Dismiss(False, True)
        if item.GetMenu() == menuInfo.GetMenu():
            idx = self._items.index(item)

    # Remove the popup menu as well
    if self._moreMenu and self._moreMenu.IsShown():
        self._moreMenu.Dismiss(False, True)

    # make sure that the menu item button is highlited
    if idx != wx.NOT_FOUND:
        self._dropDownButtonState = ControlNormal
        self._curretHiliteItem = idx
        for item in self._items:
            item.SetState(ControlNormal)

        self._items[idx].SetState(ControlFocus)
        self.Refresh()

    rect = menuInfo.GetRect()
    menuPt = self.ClientToScreen(wx.Point(rect.x, rect.y))
    menuInfo.GetMenu().SetOwnerHeight(rect.height)
    menuInfo.GetMenu().Popup(wx.Point(menuPt.x, menuPt.y), self)

Iā€™m also going to consider exporing the following event code to see what that does and if I could override it.

EVT_FLAT_MENU_ITEM_MOUSE_OVER = wx.PyEventBinder(wxEVT_FLAT_MENU_ITEM_MOUSE_OVER, 1)
ā€œā€" Fires an event when the mouse enters a :class:FlatMenuItem. ā€œā€"

Paul

I have now managanged to open a menu as a drag occours. However this seams to cancel the drag? and I cant continue to open other submenus

import wx
import wx.lib.agw.flatmenu as FM

# https://docs.wxpython.org/wx.lib.agw.flatmenu.html?highlight=flatmenu#module-wx.lib.agw.flatmenu


class MyTextDropTarget(wx.TextDropTarget):
    def __init__(self,menubar,menu):
        wx.TextDropTarget.__init__(self)
        # self.target = target
        # self.frame=frame
        self.menu = menu
        self.menubar=menubar

    def OnDropText(self, x, y, text):
        # self.window.SetLabel("(%d, %d)\n%s\n" % (x, y, text))
        print(text)
        return True

    def OnDragOver(self, x, y, d):
        print("dragover event")
        self.activate_menu_at_point((x,y))
        return wx.DragCopy

    def activate_menu_at_point(self,pt):
        MenuItem = 1
        idx, where = self.menubar.HitTest(pt)
        if where == MenuItem:
            # Position the menu, the GetPosition() return the coords
            # of the button relative to its parent, we need to translate
            # them into the screen coords
            self.menubar.ActivateMenu(self.menubar._items[idx])


class MyFrame(wx.Frame):

    def __init__(self, parent):

        wx.Frame.__init__(self, parent, -1, "FlatMenu Demo")

        self.CreateMenu()

        panel = wx.Panel(self, -1)
        btn = wx.Button(panel, -1, "TriggerMenu", (20, 20), (100, 40))
        txt = wx.TextCtrl(panel, -1, "drag this text",(20, 60), (100, 80))


        main_sizer = wx.BoxSizer(wx.VERTICAL)
        main_sizer.Add(self.menubar, 0, wx.EXPAND)
        main_sizer.Add(panel, 1, wx.EXPAND)

        self.SetSizer(main_sizer)
        main_sizer.Layout()
        self.Bind(wx.EVT_BUTTON, self.OnButton)

    def CreateMenu(self):

        self.menubar = FM.FlatMenuBar(self, -1)

        menu1 = FM.FlatMenu()
        menu2 = FM.FlatMenu()
        submenu = FM.FlatMenu()

        # Append the menu items to the menus
        menu1.Append(-1, "Item1", "Text", None)
        menu2.Append(-1, "Item2", "Text", None)
        submenu.Append(-1, "Sub1", "Text", None)
        submenu.Append(-1, "Sub2", "Text", None)
        submenu.Append(-1, "Sub3", "Text", None)

        # menu1.Append(submenu, "submenu")

        self.menubar.Append(menu1, "Menu1")
        self.menubar.Append(menu2, "Menu2")

        target = self.menubar
        text_dt = MyTextDropTarget(menubar=self.menubar, menu=menu1)
        target.SetDropTarget(text_dt)


    def OnButton(self, e):
        ''' Trying to work out how to open the menu from a button click first'''
        print("button run")
        self.TriggerMenuTest(e)


    def TriggerMenuTest(self,e):
        # First breakthough of how to trigger a menu programaticly
        self.menubar.ActivateMenu(self.menubar._items[0])

        # Ideas and bits of reference MenuBar source code
        # menu_item_index = self.menubar.FindMenu(self.f_menu.GetTitle())
        # self.menubar.ActivateMenu(self._items[menu_item_index])
        ## HitTest constants
        # NoWhere = 0
        # MenuItem = 1
        # ToolbarItem = 2
        # DropDownArrowButton = 3
        # _itemsArr
        # self._itemsArr[itemIdx]

app = wx.App(0)

frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()

app.MainLoop()

Hi Paul,

FM.FlatMenu is a subclass of PopupWindow:

>>> FM.FlatMenu.__mro__
(<class 'wx.lib.agw.flatmenu.FlatMenu'>,
 <class 'wx.lib.agw.flatmenu.FlatMenuBase'>,
 <class 'wx.lib.agw.flatmenu.ShadowPopupWindow'>,
 <class 'wx._core.PopupWindow'>,
 <class 'wx._core.NonOwnedWindow'>,
 <class 'wx._core.Window'>,
 <class 'wx._core.WindowBase'>,
 <class 'wx._core.EvtHandler'>,
 <class 'wx._core.Object'>,
 <class 'wx._core.Trackable'>,
 <class 'sip.wrapper'>,
 <class 'sip.simplewrapper'>,
 <class 'object'>)

Thus, you can set a drop target for each menu1, menu2, ā€¦ respectively.

1 Like