Duplex: Internal Error After Adding New Uitogglebutton

The Scripting Terminal told me to report this, so I’m reporting it:

  
*** .\Duplex/MessageStream.lua:117: Internal Error. Please report: unknown evt_type '11'  
*** stack traceback:  
*** [C]: in function 'error'  
*** .\Duplex/MessageStream.lua:117: in function 'add_listener'  
*** .\Duplex/UIToggleButton.lua:261: in function 'add_listeners'  
*** .\Duplex/UIToggleButton.lua:51: in function <.><br>
*** [C]: in function 'UIToggleButton'<br>
***.\Duplex/Applications/Transport.lua:281: in function '__build_app'<br>
***.\Duplex/Applications/Transport.lua:146: in function 'start_app'<br>
***.\Duplex/Browser.lua:1317: in function 'start'<br>
***.\Duplex/Browser.lua:468: in function 'start_current_configuration'<br>
***.\Duplex/Browser.lua:419: in function 'set_configuration'<br>
*** main.lua:34: in function 'create_browser'<br>
*** main.lua:66: in function 'apply_autostart_configurations'<br>
*** main.lua:173: in function <168><br>
<br>```

<br>
<br>
I was trying to add a new listener (DEVICE_EVENT_BUTTON_RELEASED) to the Duplex UIToggleButton. Then I thought maybe I was being punished for hacking core files, so I created a new UIToggleButtonWithRelease and require'd it into my Application, but I get the same error.<br>
<br>
Full modified UIToggleButtonWithRelease.lua file below in case it helps. I added a do_release method at line 79, and add_listener at 261 and a remove_listener at 281.<br>
<br>
Any ideas? I'd really like the do_release event. Thanks!<br>
<br>
[luabox]<br>
--[[----------------------------------------------------------------------------<br>
-- Duplex.UIToggleButtonWithRelease<br>
----------------------------------------------------------------------------]]--<br>
<br>
--[[<br>
<br>
Inheritance: UIComponent &gt; UIToggleButtonWithRelease<br>
Requires: Globals, Display, MessageStream, CanvasPoint<br>
<br>
About<br>
<br>
UIToggleButtonWithRelease is a simple rectangular on/off toggle button <br>
You can use it with a button, but also dial and fader input is supported: <br>
just turn the control to it's maximum or minimum to toggle between values <br>
<br>
- display as normal/dimmed version<br>
- minimum unit size: 1x1<br>
<br>
Events<br>
<br>
  on_change() - invoked whenever the button change it's active state<br>
  on_hold() - (optional) invoked when the button is held for a while<br>
<br>
<br>
--]]<br>
<br>
<br>
--==============================================================================<br>
<br>
class 'UIToggleButtonWithRelease' (UIComponent)<br>
<br>
function UIToggleButtonWithRelease:__init(display)<br>
  TRACE('UIToggleButtonWithRelease:__init')<br>
<br>
  UIComponent.__init(self,display)<br>
<br>
  -- initial state is nil (to force drawing)<br>
  self.active = nil<br>
<br>
  -- paint inverted<br>
  self.inverted = false<br>
<br>
  self._cached_active = nil<br>
<br>
  self.palette = {<br>
    foreground = table.rcopy(display.palette.color_1),<br>
    foreground_dimmed = table.rcopy(display.palette.color_1_dimmed),<br>
    background = table.rcopy(display.palette.background)<br>
  }<br>
<br>
  self.add_listeners(self)<br>
<br>
end<br>
<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
-- user input via button<br>
<br>
function UIToggleButtonWithRelease:do_press()<br>
  TRACE("UIToggleButtonWithRelease:do_press")<br>
  <br>
  if (self.on_change ~= nil) then<br>
<br>
    local msg = self:get_msg()<br>
    if not (self.group_name == msg.group_name) then<br>
      return <br>
    end<br>
    if not self:test(msg.column,msg.row) then<br>
      return <br>
    end<br>
<br>
    self.toggle(self)<br>
<br>
  end<br>
<br>
end<br>
<br>
-- ... and release<br>
<br>
function UIToggleButtonWithRelease:do_release()<br>
  TRACE("UIToggleButtonWithRelease:do_release")<br>
  <br>
  if (self.on_change ~= nil) then<br>
<br>
    local msg = self:get_msg()<br>
    if not (self.group_name == msg.group_name) then<br>
      return <br>
    end<br>
    if not self:test(msg.column,msg.row) then<br>
      return <br>
    end<br>
<br>
  end<br>
<br>
end<br>
<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
-- user input via slider, encoder<br>
<br>
function UIToggleButtonWithRelease:do_change()<br>
  TRACE("UIToggleButtonWithRelease:do_change()")<br>
<br>
  if (self.on_change ~= nil) then<br>
    local msg = self:get_msg()<br>
    if not (self.group_name == msg.group_name) then<br>
      return <br>
    end<br>
    if not self:test(msg.column,msg.row) then<br>
      return <br>
    end<br>
    -- toggle when moved away from min/max values<br>
    if self.active and msg.value &lt; msg.max then<br>
      self.toggle(self)<br>
    elseif not self.active and msg.value &gt; msg.min then<br>
      self.toggle(self)<br>
    end<br>
  end<br>
<br>
end<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
-- user input via (held) button<br>
-- on_hold() is the optional handler method<br>
<br>
function UIToggleButtonWithRelease:do_hold()<br>
  TRACE("UIToggleButtonWithRelease:do_hold()")<br>
<br>
  if (self.on_hold ~= nil) then<br>
    local msg = self:get_msg()<br>
    if not (self.group_name == msg.group_name) then<br>
      return <br>
    end<br>
    if not self:test(msg.column,msg.row) then<br>
      return <br>
    end<br>
    self:on_hold()<br>
  end<br>
<br>
end<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
-- toggle button state<br>
<br>
function UIToggleButtonWithRelease:toggle()<br>
  TRACE("UIToggleButtonWithRelease:toggle")<br>
<br>
  self.active = not self.active<br>
  self._cached_active = self.active<br>
  <br>
  self:__invoke_handler()<br>
end<br>
<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
-- set button state<br>
<br>
function UIToggleButtonWithRelease:set(value,skip_event_handler)<br>
--TRACE("UIToggleButtonWithRelease:set", value)<br>
  <br>
  if (self._cached_active ~= value) then<br>
<br>
    self._cached_active = value<br>
    self.active = value<br>
    if(skip_event_handler)then<br>
      self:invalidate()<br>
    else<br>
      self:__invoke_handler()<br>
    end<br>
  end<br>
end<br>
<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
function UIToggleButtonWithRelease:set_dimmed(bool)<br>
  if(self.dimmed == bool)then<br>
    return<br>
  end<br>
  self.dimmed = bool<br>
  self:invalidate()<br>
end<br>
<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
-- trigger the external handler method<br>
-- (this can revert changes)<br>
<br>
function UIToggleButtonWithRelease:__invoke_handler()<br>
<br>
  if (self.on_change == nil) then return end<br>
<br>
  local rslt = self:on_change()<br>
  if (rslt==false) then -- revert<br>
    self.active = self._cached_active<br>
  else<br>
    self:invalidate()<br>
  end<br>
end<br>
<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
function UIToggleButtonWithRelease:draw()<br>
  TRACE("UIToggleButtonWithRelease:draw")<br>
<br>
  local foreground,foreground_dimmed,background<br>
<br>
  if(self.inverted)then<br>
    foreground = self.palette.background<br>
    foreground_dimmed = self.palette.background<br>
    background = self.palette.foreground<br>
  else<br>
    foreground = self.palette.foreground<br>
    foreground_dimmed = self.palette.foreground_dimmed<br>
    background = self.palette.background<br>
  end<br>
  <br>
  local point = CanvasPoint()<br>
<br>
  if self.active then<br>
    if self.dimmed then<br>
      point:apply(foreground_dimmed)<br>
    else<br>
      point:apply(foreground)<br>
    end<br>
    point.val = true<br>
  else<br>
    point:apply(background)<br>
    point.val = false<br>
  end<br>
  self.canvas:fill(point)<br>
<br>
  UIComponent.draw(self)<br>
<br>
end<br>
<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
function UIToggleButtonWithRelease:add_listeners()<br>
<br>
  self.__display.device.message_stream:add_listener(<br>
    self, DEVICE_EVENT_BUTTON_PRESSED,<br>
    function() self:do_press() end )<br>
<br>
  self.__display.device.message_stream:add_listener(<br>
    self,DEVICE_EVENT_VALUE_CHANGED,<br>
    function() self:do_change() end )<br>
<br>
  self.__display.device.message_stream:add_listener(<br>
    self,DEVICE_EVENT_BUTTON_HELD,<br>
    function() self:do_hold() end )<br>
<br>
  self.__display.device.message_stream:add_listener(<br>
    self,DEVICE_EVENT_BUTTON_RELEASED,<br>
    function() self:do_release() end )<br>
<br>
end<br>
<br>
<br>
--------------------------------------------------------------------------------<br>
<br>
function UIToggleButtonWithRelease:remove_listeners()<br>
<br>
  self.__display.device.message_stream:remove_listener(<br>
    self,DEVICE_EVENT_BUTTON_PRESSED)<br>
<br>
  self.__display.device.message_stream:remove_listener(<br>
    self,DEVICE_EVENT_VALUE_CHANGED)<br>
<br>
  self.__display.device.message_stream:remove_listener(<br>
    self,DEVICE_EVENT_BUTTON_HELD)<br>
    <br>
  self.__display.device.message_stream:remove_listener(<br>
    self,DEVICE_EVENT_BUTTON_RELEASED)<br>
<br>
end<br>
<br>
[/luabox]</168></.>

The error happens because the DEVICE_EVENT_BUTTON_RELEASED is an unexpected type of event. So much is obvious, but I think you should be able to work around that by modifying the MessageStream class, if you are not afraid of a bit of hacking.

But I have to ask what you are trying to achieve with the release event? After all, we are talking about a toggle button, which does not need a release event - at least not if you ask me. If you are trying to make a new type of button, it would perhaps be better to name it something more obviously different like UIPushButton?

I think each type of buttons need a clearly defined purpose, like this:

UIToggleButton - toggles between enabled and disabled state (used with the Matrix and a couple of other places)
UITriggerButton - activate/briefly light up when pressed (used with the Transport class)
…more?

I sort of assumed that since UIToggleButton inherits from UIComponent that it would have all the events that UIComponent does… but I guess Lua inheritance doesn’t work that way? Or are you saying there is no such event as DEVICE_EVENT_BUTTON_RELEASED? It’s listed in the Duplex.API.txt documentation.

What I’m trying to achieve is basically exactly the functionality of your ToggleButton - so when you press & release the button quickly it toggles state - but also to be able to track if groups of buttons or individual buttons are held down. So that for use in a step sequencer, for instance, you can hold down a few different steps and then use other controllers to modify those steps. Kind of like “selecting” an arbitrary group of steps by holding them down. To do this I’m thinking I’d need to add the button to a “hold tracking” table on_press, and then remove it from the table on_release.

My thinking was that a button is a button (is a button). All buttons can be pressed, released, held, etc. But the specifics of how that button interacts with an application would be defined by the application.

But of course, it’s your framework :) So I’ll start a new kind of button from the ground up. UIGeneralPurposeButton! :) I’ll check out the MessageStream class when I get home.

Thanks!

Yeah, the event exists but it’s not used anywhere. If you want to see some more detailed info, it’s possible to study a previous revision like r493. Here there’s a lot more comments that (hopefully) make it clear why the release event has not yet gotten a full integration into Duplex.

But it basically boils down to the following: we didn’t have a press() and release() event for native Renoise UI widgets until shortly before the launch. Since then, it has gotten a lot more interesting to make more advanced interactions, since we don’t need to make a special case for the virtual control surface, but can run the same code anywhere. This is not that hard to do, just yet another thing on the todo list.

Personally, I would like to make a custom UI widget for the Matrix sequence control (the buttons next to the matrix controlling the playback position). With a proper release event in place, we could do stuff like pressing one buttons, then pressing another and releasing both to loop a specific region. Add some flashing lights for scheduled patterns, and other cool stuff…However, it would need to be a separate mode/option, because then you wouldn’t be able to hit a button to trigger the pattern!! With such a looping feature, you would need to release the button first, which is not what some people might want/expect. We need to be really careful about changing stuff that’s already working…

Hey, thanks for all the info, but I’m unfortunately not clear on why the release event can’t be easily implemented. You seem to have a case in the MessageStream:input_message function where you have established that the button was released. Why not just fire the release_listeners here?

  
-- for toggle buttons, broadcast releases to listeners as well  
if (msg.input_method == CONTROLLER_TOGGLEBUTTON) then  
 for _,listener in ipairs(self.press_listeners) do   
 listener.handler()   
 end  
else -- elseif (msg.input_method == CONTROLLER_BUTTON) then  
 for _,listener in ipairs(self.release_listeners) do   
 listener.handler()   
 end  
end  
  

This seems to be kind of working for me, in that when I press a button on the Launchpad, my on_press handler fires, and likewise when I release a button the on_release handler fires. My issue now is that ALL the buttons fire on any one press or release - I now see what the test(x_pos, y_pos) function is for, but it doesn’t seem to be working as I had expected.

I think I’m going to need to dive into the debugger to try to see what is going on with these messages.

EDIT: Nevermind, I was stupid and had replaced do_press and do_release instead of defining on_press and on_release. :S All good.