Menu with checkmark



  • Many menus in Poser place a checkmark next to the current item. However, I couldn't find a way to create a menu in python that can do the same. The only documented option I see is the "AskMenu" dialog method. It has the same margin space along the left side where a checkmark could be placed, the same as other menus, but it seems that feature was exposed. I don't see any other way to do it either. Any ideas?



  • @semicharm the practice I have used in this circumstance is to indicate the default choice or current selection in the Message parameter of DialogSimple.AskMenu(), like:

    selection = 'Item 2'
    dlg = poser.DialogSimple.AskMenu('Title','Select Item [{}]'.format(selection),('Item 1','Item 2','Item 3'))
    

    which appears as:
    0_1557388275862_Screen Shot 2019-05-09 at 5.49.22 pm.png 0_1557388653968_Screen Shot 2019-05-09 at 5.57.05 pm.png
    That means that it's quite clear, without even opening the menu popup hierarchy, what will be selected if you just click OK.

    Having dealt with Mac Menu definition files, which the Poser UI xml files bear a resemblance to, there certainly ought to be a particular character sequence which can indicate such a check mark, though I'd have to delve into packing boxes to try and find my old Inside Macintosh documentation. It might also be possible to hook into the wx structures and update the menu, but you might have more certainty building your own menu dialog.



  • I thought the same thing and I tested all 256 characters at both the beginning and end of the items. None prompted Poser to produce a checkmark, so I don't know what the special sequence could be...assuming it even has one. It may very well require using an XML, which I investigated for other reasons. However, the structure was just so unintuitive that I didn't make any headway. I think some other environments used a parameter to specify a default or selected item. Oh well... I probably have to learn wx eventually, just not for this.

    For the time being, I put an asterisk before the current selection and a space before the others to balance them out visually. It just seems ridiculous and redundant considering that the UI already is able to handle this.



  • @semicharm that's very true and frustrating that we're tempted by what we can see should be possible and what's actually been exposed to Python. It's great that a few simple and useful UI tools have been made available, especially since there was an inter-platform battle over tkInter and it's broken and orphaned implementation on MacOS, but at least we have wxAUI.

    My first efforts to translate a useful tkInter script into wx were quickly abandoned, until I later found a comparable example (like a Rosetta Stone) that helped me mentally translate what was being done in tkInter into wx. For quick and nasty, it's still hard to go past the DialogSimple methods, though.



  • @anomalaus No kidding! LOL The checkmark issue wasn't really a necessity till now. The entire UI on this one was distilled down to two menus and optionally a PickImage dialog. I don't feel like putting the project off any longer just to learn wx for something so simple. I've done UI programing in C, Java, and JavaScript back in the day, so I'm familiar with all of the concepts. It's just learning the implementation in some environments that's a steep hill to climb.



  • I made a set of dialogs.

    The following scrollable dialog:

    0_1557435816509_Bildschirmfoto von 2019-05-09 22-55-50.png
    Needs this code:

        def isNotHidden(obj):
            return not obj.Hidden()
    
        selected = SelectParametersDialog(control="checkbutton",
                                          filter=dict(IsValueParameter=True),
                                          state=isNotHidden,
                                          title="Value Parameters",
                                          header="Select one or more items...",
                                          footer="...and click OK to submit or CANCEL to abort."
                                          )
        if selected:
            print("SelectParametersDialog result:", selected)
    

    Here is the complete code with several dialogs:

    from __future__ import print_function
    
    try:
        import poser
        SCENE = poser.Scene()
    except poser.error:
        raise RuntimeError("Script must run in Poser.")
    
    import wx
    from wx.lib.scrolledpanel import ScrolledPanel
    
    wx.Dialog.EnableLayoutAdaptation(True)
    
    FILTER = dict
    
    class _LongMessageBox(wx.Dialog):
        """
        Used by function MessageBox([...])
    
        Set up a Message Dialog. Message can be multiline and has
        scrollbars if required.
        Parameters:
            message: Main Message.
            header: Text above message.
            footer: Text below message.
        """
        def __init__(self, *args, **kwargs):
            self.text = kwargs.pop("message", kwargs.pop("Message", None))
            self.header = kwargs.pop("header", kwargs.pop("Header", None))
            self.footer = kwargs.pop("footer", kwargs.pop("Footer", None))
    
            wx.Dialog.__init__(self, *args, **kwargs)
            self.InitUI()
    
        def InitUI(self):
            btn_ok = wx.Button(self, id=wx.ID_OK)
            btn_ok.SetDefault()
    
            sizer = wx.BoxSizer(wx.VERTICAL)
            btn_sizer = wx.StdDialogButtonSizer()
            btn_sizer.AddButton(btn_ok)
            btn_sizer.Realize()
            if self.header is not None:
                sizer.Add((1, 10), 0)
                h = wx.StaticText(self, label=self.header)
                font = self.GetFont()
                h.SetFont(font.Bold())
                sizer.Add(h, 0, wx.ALIGN_CENTER_HORIZONTAL)
    
            tx = wx.TextCtrl(self, size=self.GetSize(), value=self.text,
                             style=wx.TE_MULTILINE | wx.TE_BESTWRAP | wx.TE_READONLY)
    
            sizer.Add(tx, 1, wx.EXPAND)
    
            if self.footer is not None:
                h = wx.StaticText(self, label=self.footer)
                font = self.GetFont()
                h.SetFont(font.Bold())
                sizer.Add(h, 0, wx.ALIGN_CENTER_HORIZONTAL)
                sizer.Add((1, 16), 0)
    
            sizer.Add(btn_sizer, 0, wx.EXPAND)
            self.SetSizer(sizer)
            sizer.Fit(self)
    
    
    def MessageBox(message, **kwargs):
        """
        Display a Multiline-Message-Dialog.
        :param message: Message text.
        :param kwargs: dictionary with keywords:
                        style = Optional, additional style.
                        header = Optional, Text displayed above message.
                        footer = Optional, Text displayed below message.
        :return: Result of Dialog.ShowModal()
        """
        man = poser.WxAuiManager()
        root = man.GetManagedWindow()
    
        style = kwargs.pop("style", 0)
        with _LongMessageBox(parent=root, style=wx.DEFAULT_DIALOG_STYLE | style,
                             message=message, **kwargs) as dlg:
            res = dlg.ShowModal()
        return res
    
    
    class _SelectDialog(wx.Dialog):
        """
        Used by function SelectDialog([...])
    
        Set up a dialog to choose or check from a list of items.
        Parameters:
            select_ctrl: wx.CheckBox or wx.RadioButton (choose or check).
            labels: List of <str> labels to choose from.
            header: Text above message.
            footer: Text below message.
        """
        def __init__(self, select_ctrl=wx.CheckBox, *args, **kwargs):
            self.labels = kwargs.pop("labels", kwargs.pop("Labels", list()))
            self.header = kwargs.pop("header", kwargs.pop("Header", None))
            self.footer = kwargs.pop("footer", kwargs.pop("Footer", None))
            self.ctrls = []
            self.select_ctrl = select_ctrl
            self.closeOnClick = kwargs.pop("closeOnClick", False)
    
            wx.Dialog.__init__(self, *args, **kwargs)
            self.InitUI()
    
        def InitUI(self):
            sizer = wx.BoxSizer(wx.VERTICAL)
            btn_sizer = wx.StdDialogButtonSizer()
            if not self.closeOnClick:
                btn_ok = wx.Button(self, id=wx.ID_OK)
                btn_ok.SetDefault()
                btn_sizer.AddButton(btn_ok)
    
            btn_close = wx.Button(self, id=wx.ID_CANCEL)
            btn_close.SetDefault()
            btn_sizer.AddButton(btn_close)
            btn_sizer.Realize()
            if self.header is not None:
                sizer.Add((1, 10), 0)
                h = wx.StaticText(self, label=self.header)
                font = self.GetFont()
                h.SetFont(font.Bold())
                sizer.Add(h, 0, wx.ALIGN_CENTER_HORIZONTAL)
    
            panel = ScrolledPanel(self, size=self.GetSize(), style=wx.SIMPLE_BORDER)
            panel.SetupScrolling()
            ctrl_sizer = wx.BoxSizer(wx.VERTICAL)
            for idx, entry in enumerate(self.labels):
                label, state = entry
                self.ctrls.append(self.select_ctrl(panel, label=label))
                self.ctrls[idx].index = idx
                self.ctrls[idx].state = state
                if state is not None:
                    self.ctrls[idx].SetValue(state)
    
                ctrl_sizer.Add(self.ctrls[idx], 0, wx.ALIGN_LEFT | wx.EXPAND)
    
            panel.SetSizer(ctrl_sizer)
            sizer.Add(panel, 1, wx.EXPAND)
    
            if self.footer is not None:
                h = wx.StaticText(self, label=self.footer)
                font = self.GetFont()
                h.SetFont(font.Bold())
                sizer.Add(h, 0, wx.ALIGN_CENTER_HORIZONTAL)
                sizer.Add((1, 16), 0)
    
            if self.closeOnClick:
                self.Bind(wx.EVT_RADIOBUTTON, self.onClick)
    
            sizer.Add(btn_sizer, 0, wx.EXPAND)
            self.SetSizer(sizer)
            sizer.Fit(self)
    
        def onClick(self, ev):
            wx.CallLater(100,  self.EndModal(wx.ID_OK))
    
    def SelectDialog(*selections, **kwargs):
        """
        Display a Selection-Dialog.
        :param selections: List of Strings to display in selectionlist.
        :param kwargs: <dict> with keywords/values:
                        control: <wx.CheckBox> or <wx.RadioButton> (choose or check)
                        closeOnClick: <bool>, close dialog after an item is clicked.
    
        :return: List of clickstatus for each item in <selections>.
    
        E.g.:
            sel = SelectDialog(["AAA", "BBB", "CCC"], wx.RadioButton, True)
            print("Result:", sel)
            >> Result: [0, 1, 0] # (if "BBB" is checked)
        """
        control = kwargs.pop("control", None)
        if control is None:
            control = wx.CheckBox
        if callable(control):
            control = control.__class__
        if isinstance(control, str):
            if control.lower().startswith("radio"):
                control = wx.RadioButton
            elif control.lower().startswith("check"):
                control = wx.CheckBox
    
        if control is wx.RadioButton and not "closeOnClick" in kwargs:
            kwargs["closeOnClick"] = True
    
        if len(selections) and isinstance(selections[0], (list, tuple)):
            selections = selections[0]
    
        man = poser.WxAuiManager()
        root = man.GetManagedWindow()
    
        style = kwargs.pop("style", 0)
        with _SelectDialog(parent=root,
                           select_ctrl=control,
                           style=wx.DEFAULT_DIALOG_STYLE | style,
                           labels=selections, **kwargs) as dlg:
            res = dlg.ShowModal()
            if res == wx.ID_OK:
                return [selections[i][0] for i, c in enumerate(dlg.ctrls) if c.GetValue()==1]
    
        return None
    
    
    def poser_obj_filter(iterobj, filter_obj, all_or_any=all):
        assert all_or_any == all or all_or_any == any, \
            "Use Python keywords 'all' or 'any'.\n" \
            "E.g.: obj_filter(<iterable iterobj>, <dict filter_dict>, all_or_any=any)"
    
        if isinstance(filter_obj, dict):
            return all_or_any([v(getattr(iterobj, f)()) if callable(v)
                               else getattr(iterobj, f)() == v
                               for f, v in filter_obj.items()]
                              )
    
        if filter_obj is not None:
            # filterlist has to be <type dict>.
            assert isinstance(filter_obj, dict), \
                "Filterlist must be <type dict>, not %s." % type(filter_obj)
    
        # nothing to filter, return iter-object
        return iterobj
    
    
    def objectlist(iterobj, filter, all_or_any=all):
        return (obj for obj in iterobj if poser_obj_filter(obj, filter, all_or_any))
    
    
    def namelist(iterobj, filter, state=None, all_or_any=all):
        if callable(state):
            return sorted([obj.Name(), state(obj)] for obj in objectlist(iterobj, filter, all_or_any))
        elif isinstance(state, bool):
            return sorted([obj.Name(), state] for obj in objectlist(iterobj, filter, all_or_any))
        else:
            return sorted([obj.Name(), None] for obj in objectlist(iterobj, filter, all_or_any))
    
    def SelectParametersDialog(*args, **kwargs):
        names = namelist(SCENE.CurrentActor().Parameters(),
                         state=kwargs.pop("state", None),
                         filter=kwargs.pop("filter", FILTER()))
        return SelectDialog(names, *args, **kwargs)
    
    
    def SelectMorphsDialog(*args, **kwargs):
        kwargs.setdefault("control", "check")
        kwargs.setdefault("closeOnClick", False)
        kwargs.setdefault("title", "Select Morphs")
        kwargs.setdefault("filter", FILTER())["IsMorphTarget"] = True
        return SelectParametersDialog(*args, **kwargs)
    
    
    def SelectValueParmsDialog(*args, **kwargs):
        filter = kwargs.pop("filter", FILTER()).update(FILTER(IsValueParameter=True))
        state = kwargs.pop("state", None)
        names = namelist(SCENE.CurrentActor().ValueParameters(), filter=filter, state=state)
        return SelectDialog(names, *args, **kwargs)
    
    
    def SelectActorsDialog(*args, **kwargs):
        state = kwargs.pop("state", None)
        filter=kwargs.pop("filter", FILTER())
        names = namelist(SCENE.Actors(), filter=filter, state=state)
        return SelectDialog(names, *args, **kwargs)
    
    
    def SelectLightsDialog(*args, **kwargs):
        state = kwargs.pop("state", None)
        filter=kwargs.pop("filter", FILTER()).update(FILTER(IsLight=True))
        names = namelist(SCENE.Lights(), filter=filter, state=state)
        return SelectDialog(names, *args, **kwargs)
    
    
    def SelectCamerasDialog(*args, **kwargs):
        state = kwargs.pop("state", None)
        filter = kwargs.pop("filter", FILTER())
        names = namelist(SCENE.Cameras(), filter=filter, state=state)
        return SelectDialog(names, *args, **kwargs)
    
    
    def SelectDeformersDialog(*args, **kwargs):
        state = kwargs.pop("state", None)
        filter = kwargs.pop("filter", FILTER()).update(FILTER(IsDeformer=True))
        names = namelist(SCENE.Actors(), filter=filter, state=state)
        return SelectDialog(names, *args, **kwargs)
    
    
    def SelectMagnetsDialog(*args, **kwargs):
        state = kwargs.pop("state", None)
        filter=dict(IsBase=False, IsZone=False, IsDeformer=True)
        names = namelist(SCENE.Actors(), filter=filter, state=state)
        return SelectDialog(names, *args, control="check", **kwargs)
    
    
    if __name__ == "__main__":
    
        ### Some functions useful for "state":
        def isCurrent(obj):
            return SCENE.CurrentActor() == obj
        def isHidden(obj):
            return obj.Hidden()
        def isNotHidden(obj):
            return not obj.Hidden()
    
        selected = SelectParametersDialog(control="checkbutton",
                                          filter=dict(IsValueParameter=True),
                                          state=isNotHidden,
                                          title="Value Parameters",
                                          header="Select one or more items...",
                                          footer="...and click OK to submit or CANCEL to abort."
                                          )
        if selected:
            print("SelectParametersDialog result:", selected)
    
        def MatchFigure(fig):
            return fig.InternalName() == SCENE.CurrentFigure().InternalName() \
                if fig else False
    
        selected = SelectActorsDialog(control="radiobutton",
                                      filter=dict(IsBodyPart=True,
                                                  ItsFigure=MatchFigure),
                                      state=isCurrent,
                                      closeOnClick=True
                                      )
        if selected:
            print("SelectActorsDialog:", selected)
    
        selected = SelectMagnetsDialog()
        if selected:
            print("SelectMagnetsDialog:", selected)
    
        def light_status(obj):
            return obj.LightOn()
    
        selected = SelectLightsDialog(
                control="checkbutton",
                state=light_status,
                closeOnClick=False,
                title="Click to switch Lighth on/off",
                footer="Light is set after OK is clicked.",
                size=(200, 100)
        )
        if selected:
            for light in SCENE.Lights():
                light.SetLightOn(light.Name() in selected)
    
            print("SelectLightsDialog:", selected)
        SCENE.DrawAll()
    
    


  • @adp that's great! While I was trawling through the documentation on MacOS UI concepts, I discovered that there was a distinction made between three types of menu item (four, if you include a separator, for ordinary Application Menu Bar menus): Normal, Checkable (which is what you show above, for making multiple selections), and RadioButton (for making mutually exclusive selections). The check mark, as opposed to a checkbox, is supposedly reserved for indicating a current selection, or more exactly, an active, enabled item. For disabled items, the font gets grayed or dimmed and is unselectable. [EDIT: Now that I've scrolled through your code, I see that that's pretty much what you already have, except perhaps for the checkmark (tick) without a box :-)]

    Had you thought about adding 'Select None' and 'Select All' buttons, to mirror what some of Poser's own multiple hierarchy selection menus offer? Or an option/alt selection to propagate changes to sub-items?



  • @anomalaus said in Menu with checkmark:

    @adp that's great! While I was trawling through the documentation on MacOS UI concepts, I discovered that there was a distinction made between three types of menu item (four, if you include a separator, for ordinary Application Menu Bar menus): Normal, Checkable (which is what you show above, for making multiple selections), and RadioButton (for making mutually exclusive selections). The check mark, as opposed to a checkbox, is supposedly reserved for indicating a current selection, or more exactly, an active, enabled item. For disabled items, the font gets grayed or dimmed and is unselectable. [EDIT: Now that I've scrolled through your code, I see that that's pretty much what you already have, except perhaps for the checkmark (tick) without a box :-)]

    Had you thought about adding 'Select None' and 'Select All' buttons, to mirror what some of Poser's own multiple hierarchy selection menus offer? Or an option/alt selection to propagate changes to sub-items?

    I take it that those other options would require XML or wx to implement?



  • @semicharm yes. With @adp 's code, you can roll your own menu dialog. I'm guessing that the Poser devs probably didn't build into their DialogSimple.AskMenu() method any provisions for reading the format from an XML file, or bother with the niceties of parsing each menu item for escape characters to define check marks or display keyboard shortcuts, since there's another place to configure those, but it's incompatible with user created dynamic windows from Python.

    It might just be possible to poke around in the exposed methods of the dialog returned from the AskMenu() call, but there's no documentation and no guarantee anything tried there would remain supported, even if it did work.

    Bits of working code, such as what @adp has just gifted us with are definitely the best way to get one's head around wx for Poser, though.



  • @anomalaus The DialogSimple methods are just function calls, they don't return the dialog as an object that can be inspected like some of the others. The generic Dialog method is the one that takes an XML format. It would be helpful if they included some kind of documentation, but no such luck. I tried using it and got something to popup after using the "Show" method (which they didn't bother to document). Looking again at one of the XML files the header says "wxWidgetCanvas". Maybe the format would make more sense if I knew how wxWidget Canvas worked, but not much luck on google. Maybe someone would find that info useful...



  • @adp Thanks for the reference code! It could be useful on another project I have planned, as using the basic AskMenu is getting to be a PITA on that one. I'd probably replace the checkboxes with a simpler multiselect list though. (BTW checkMARK and checkBOX are two entirely different things LOL) For the current applet, a simple dropdown list is all I need...even AskMenu's sparse implementation it's less than ideal.



  • @anomalaus There is a difference between Menu items and Disalog controls in wxPython.

    Maybe you want a Popup-Menu insteed of a Dialog?



  • @anomalaus said in Menu with checkmark:

    @semicharm yes. With @adp 's code, you can roll your own menu dialog. I'm guessing that the Poser devs probably didn't build into their DialogSimple.AskMenu() method any provisions for reading the format from an XML file, or bother with the niceties of parsing each menu item for escape characters to define check marks or display keyboard shortcuts, since there's another place to configure those, but it's incompatible with user created dynamic windows from Python.

    For me it looks like Poser-Devs simply included some "ready-to-go" standard dialogs from wxPython into Poser.

    It might just be possible to poke around in the exposed methods of the dialog returned from the AskMenu() call, but there's no documentation and no guarantee anything tried there would remain supported, even if it did work.

    Have a look into wxPythons folder in [poserdir]\Runtime\Python\Lib\site-packages\wx-2.9.1-msw\wx. There is a lot of comments in the code.

    Online documentation for wxPython is also available (attention: Poser is using version 2.9.1, current version is 4.1).
    Mac-Support for in 2.9 isn't up to date, even windows has some problems (I made an App with layers to mark UV-maps – works perfect with Linux but not with Windows, because wxPython 2.9 for Windows has remarkable problems with transparency).



  • @semicharm I'm going to make some more example-code with wxPython. Seems there is a need for this :)



  • Some things to note:

    In wxPython a "window" is simpy a rectangle area on screen.
    What we desktop-user know as "window" is a "Frame" in wxPython.
    But a frame is indeed only a frame: It handles things around "Panels" – like window-borders, window-caption, handles to make a window (frame) bigger or smaler, and a frame holds menus, tool- and statusbars.
    A "Panel" is where most things happen: Controls (like Buttons, Checkboxes) and even other Panels (if a Panel is used as a container) usualy have a Panel as parent.

    Poser is based on "Panes", not Frames. Panes can be seen als a specialized type of Frame, used for automatically layouted UI's (AUI-Manager). One should avoid using Frames with Poser. Poser may crash :)

    Another specialised "Frame" is a Dialog. Dialogs are usally forced to stay on top of all other Frames. Dialogs can be used without problems in windows (as long as their parent-window is a Pane managed by Poser).

    What does this all mean?

    With Poser-Python one has to use a Poser managed pane as the base of all user created "Windows":

        man = poser.WxAuiManager()
        root = man.GetManagedWindow()
        myPanel = wx.Panel(parent=root, ...)
    


  • Here is a commented piece of source for a listbox with actornames currently in use by Poser.
    You can move the window around and add it to the UI like any window owned by Poser (like Hirarchy-Editor etc.).
    0_1557592776285_Bildschirmfoto von 2019-05-11 18-35-01.png 0_1557592807060_Bildschirmfoto von 2019-05-11 18-37-18.png

    from __future__ import print_function
    
    try:
        import poser
    
        SCENE = poser.Scene()
    except poser.error:
        raise RuntimeError("Script must run in Poser.")
    
    import wx
    import wx.aui  # We need this to manage Poser's Panes.
    
    APP_NAME = "Poser Test Panel"
    APP_SIZE = (800, 600)
    BG_COLOR = 75, 75, 75
    FG_COLOR = 200, 200, 200
    
    
    ##############################################################################
    # The main window for our test.
    
    class AppPanel(wx.Panel):
        def __init__(self, parent,
                     name=APP_NAME,
                     actorlist=list(),
                     size=APP_SIZE,
                     style=wx.DEFAULT | wx.NO_BORDER | wx.FRAME_TOOL_WINDOW,
                     ):
            super(AppPanel, self).__init__(parent=parent,
                                           id=wx.NewId(),
                                           name=name,
                                           style=style,
                                           size=size)
    
            self.SetBackgroundColour(BG_COLOR)
            self.SetForegroundColour(FG_COLOR)
    
            # A sizer to handle our control, vertically arranged.
            # The sizer is useful even for one single item because
            # it handles scrollbars for us.
            sizer = wx.BoxSizer(wx.VERTICAL)
    
            # Create "choices" for left listbox
            choices = [ac.Name() for ac in actorlist]
    
            # Listbox style: Extended multiple selection and
            # vertical scrollbars only when needed.
            lb_style = wx.LB_EXTENDED | \
                       wx.LB_MULTIPLE | \
                       wx.LB_NEEDED_SB
    
            self.listbox = wx.ListBox(parent=self, id=-1,
                                      choices=choices,
                                      style=lb_style)
    
            # After creating the listbox we place it into the sizer.
            sizer.Add(self.listbox, 1, wx.ALL | wx.EXPAND)  # expand control as far as possible.
            sizer.Layout()  # Let wxPython arange and resize the control.
    
            # Add the sizer to this panel.
            self.SetSizerAndFit(sizer)
    
    
    ##############################################################################
    
    aui = poser.WxAuiManager()
    root = aui.GetManagedWindow()
    
    # Create our window
    OurMainPanel = AppPanel(parent=root,
                            name=APP_NAME,
                            actorlist=SCENE.Actors())
    
    # Now get a pane (a window/frame) from Poser's AUI-Manager
    pane = wx.aui.AuiPaneInfo()
    
    # We can customize the pane a bit:
    pane.Resizable().Float().FloatingSize(wx.Size(200, 400)).BestSize(wx.Size(100, 200))
    # As you can see, each call returns the pane so we can build chains.
    # The convertinal way is:
    pane.PaneBorder(True)
    pane.Movable(False)
    pane.CaptionVisible(True)
    pane.DestroyOnClose(True)
    pane.PinButton(True)
    # Could also be written as: pane.PaneBorder(True).Movable(False)....
    
    # After this is done we just have to add our Panel to the pane we got from AUI-manager
    # and give up management to Poser-wxPython.
    aui.AddPane(OurMainPanel, pane)
    pane.Show()
    aui.Update()
    
    


  • There is a small error in the code above. The check if Poser is available is wrong. It shuld look like:

    try:
        import poser
        SCENE = poser.Scene()
    except ImportError:
        raise RuntimeError("Script must run in Poser.")
    

    (insteed of checking poser.error)



  • The second demo code is using events to react on mouseclicks (selections):

    from __future__ import print_function
    
    try:
        import poser
    
        SCENE = poser.Scene()
    except ImportError:
        raise RuntimeError("Script must run in Poser.")
    
    import wx
    import wx.aui  # We need this to manage Poser's Panes.
    import wx.dataview
    
    APP_NAME = "Poser Test Panel"
    APP_SIZE = (800, 600)
    BG_COLOR = 75, 75, 75
    FG_COLOR = 200, 200, 200
    
    
    ##############################################################################
    # The main window for our test.
    
    class AppPanel2(wx.Panel):
        def __init__(self, parent,
                     name=APP_NAME,
                     actorlist=list(),
                     size=APP_SIZE,
                     style=wx.DEFAULT | wx.NO_BORDER | wx.FRAME_TOOL_WINDOW,
                     ):
            super(AppPanel2, self).__init__(parent=parent,
                                           id=wx.NewId(),
                                           name=name,
                                           style=style,
                                           size=size)
    
            self.SetBackgroundColour(BG_COLOR)
            self.SetForegroundColour(FG_COLOR)
    
            # A sizer to handle our control, vertically arranged.
            # The sizer is useful even for one single item because
            # it handles scrollbars for us.
            sizer = wx.BoxSizer(wx.VERTICAL)
    
            # Listbox style is single selection (standard)
            # because we want to be informed for any single
            # selection that happens.
            lb_style = wx.LB_NEEDED_SB
    
            # Because we want to fill the box with objects (see below),
            # we leave "coices" empty.
            self.listbox = wx.ListBox(parent=self, id=-1,
                                      choices=[],
                                      size=wx.DefaultSize,
                                      style=lb_style)
            self.listbox.SetBackgroundColour(BG_COLOR)
            self.listbox.SetForegroundColour(FG_COLOR)
    
            # Prepare the listbox to hold some useful data.
            for ac in actorlist:
                entry = dict(name=ac.Name(),
                             iname=ac.InternalName(),
                             onoff=ac.OnOff())
                self.listbox.Append(entry.get("name"), entry)
    
            # To do something useful with the listbox, we need to
            # know if the user clicks on an entry.
            self.listbox.Bind(wx.EVT_LISTBOX, self.onSelection)
    
            # After creating the listbox we place it into the sizer.
            sizer.Add(self.listbox, 1, wx.ALL | wx.EXPAND)  # expand control as far as possible.
    
            # Create a close-button:
            self.closebtn = wx.Button(self, -1, 'Close')
            # Trigger event if button is pressed:
            self.closebtn.Bind(wx.EVT_LEFT_UP, self.onClose)
            # Add button to sizer:
            sizer.Add(self.closebtn, 0, wx.ALIGN_CENTER_HORIZONTAL)
    
            sizer.Layout()  # Let wxPython arange and resize the control.
    
            # Add the sizer to this panel.
            self.SetSizerAndFit(sizer)
    
            # Another useful event is when the window is closed by the user.
            self.Bind(wx.EVT_CLOSE, self.onClose)
            self.Bind(wx.EVT_WINDOW_DESTROY, self.onDestroy)
    
        def onSelection(self, event):
            """
            This function is called if an item is selected/deselcted.
            Parameter <event> has more information (see wxPython documention
            about events).
            """
            selection = self.listbox.GetStringSelection()
            print("onSelection:", selection)
            if selection and selection.lower() != "universe":
                # Get the object we saved in this list item.
                # GetSelection() returns <int>index-position.
                obj = self.listbox.GetClientData(self.listbox.GetSelection())
                # Remember: the object is a dictionary.
                iname = obj.get("iname")
                onoff = obj.get("onoff")
                # to show that anything works, we select this actor
                actor = SCENE.ActorByInternalName(iname)
                SCENE.SelectActor(actor)
                SCENE.DrawAll()
    
        def onClose(self, event):
            print("Closebutton pressed:", self.listbox.GetSelections())
            self.Parent.Close()
    
        def onDestroy(self, event):
            # Called if this window is destroyed
            print("On Destroy:", self.listbox.GetSelections())
            event.Skip()
    
    ##############################################################################
    
    aui = poser.WxAuiManager()
    root = aui.GetManagedWindow()
    
    # Create our window
    OurMainPanel = AppPanel2(parent=root,
                            name=APP_NAME,
                            actorlist=SCENE.Actors())
    
    # Now get a pane (a window/frame) from Poser's AUI-Manager
    pane = wx.aui.AuiPaneInfo()
    
    # We can customize the pane a bit:
    pane.Resizable().Float().FloatingSize(wx.Size(200, 400)).BestSize(wx.Size(100, 200))
    # As you can see, each call returns the pane so we can build chains.
    # The convertinal way is:
    pane.PaneBorder(True)
    pane.Movable(False)
    pane.CaptionVisible(True)
    pane.DestroyOnClose(True)
    pane.PinButton(True)
    # Could also be written as: pane.PaneBorder(True).Movable(False)....
    
    # After this is done we just have to add our Panel to the pane we got from AUI-manager
    # and give up management to Poser-wxPython.
    aui.AddPane(OurMainPanel, pane)
    pane.Show()
    aui.Update()
    
    


  • Thanks @adp, this is great stuff.

    It's very logical that one will have complete control over, and access to all features of an interface that one has created directly oneself, but are the controls and widgets of any of the built-in Poser panes accessible?

    I have been struggling with the RayTrace Preview panel, which I can get by name from the aui manager, but the only panes I can access meaningfully are the primary one I need (the actual RayTrace, which I call an OS routine to save to a PNG file) and the miniframe which holds the OS close/minimise/expand buttons. What I really need to be able to interrogate and manipulate are the Auto-update check box, the Refresh button, and the progress indicator, so I can initiate a preview raytrace, detect when it has finished and then save it in one operation, rather than relying on the user to Refresh and then run a script to save.

    I've been able to determine how to find out whether the pane is visible:

    RTPName = 'RayTracePreview'
    # Get the RayTrace Preview window's content bounding box
    bbox = None
    aui = poser.WxAuiManager()
    rtpPane = aui.GetPane( RTPName )
    # Ought to check whether pane is visible before committing to save it's content
    if not rtpPane.IsShown():
    	rtpPane.Show()
    	aui.Update()
    xpos, ypos, width, height = rtpPane.window.ClientRect
    left = xpos + ( width - RTPWidth ) / 2 # Image centred in pane
    top = ypos + ( height - RTPHeight ) / 2 # Image centred in pane
    left, top = rtpPane.window.ClientToScreen( ( left, top ) )
    right = left + RTPWidth
    bottom = top + RTPHeight
    if debug:
    	print xpos, ypos, width, height
    	print left, top, right, bottom
    bbox = ( left, top, right, bottom )
    

    Any clues, or is this actually impossible?



  • @anomalaus said in Menu with checkmark:

    It's very logical that one will have complete control over, and access to all features of an interface that one has created directly oneself, but are the controls and widgets of any of the built-in Poser panes accessible?

    Theoretically: yes. In practice it can be a bit difficult to realize.

    What I really need to be able to interrogate and manipulate are the Auto-update check box, the Refresh button, and the progress indicator, so I can initiate a preview raytrace, detect when it has finished and then save it in one operation, rather than relying on the user to Refresh and then run a script to save.

    It is not sure if the Poser-GUI is done with Python or C. If it is written in C, you can't reach Python-like objects. On the other side, WX (and wxPython) is based on events.

    To find out more, you would have to bend the event handler of the relevant Panes to your own functions.
    Problem is, the eventhandler and other relevant functions to handle a control can reside in a totally different pane/window (control is in Pane A, but is controlled by functions in Pane B).

    May I ask what you want to archive?



  • @adp I want to initiate a RayTrace Preview, wait till it has completed, and then call an OS routine to screen capture it and save it as a PNG file in a single action. I can currently save a completed RT Preview, but have to manually click to Refresh and wait till it finishes.

    The entire scenario is necessary because QueueManager Renders cannot replicate Live or background renders because the save-to-file process for the queue corrupts the file, leaving pokethrough artifacts, because it re-orders inherited deformer evaluation orders during scene save and load. This is a bug which has been around since Poser Pro 2014, at least, but no one notices because they don't push the envelope quite as hard in the particular directions I do ;-) It also took me years to figure out that it wasn't my fault that fitting poses didn't work correctly when applied to re-opened, old scene files.