Messing with userwindows

dicene
Posts: 47
Joined: Thu Sep 25, 2014 7:36 am

Re: Messing with userwindows

Post by dicene »

So, I saw you mention something about pushing out another preview in 3 weeks or so. I know that you're focusing on bug-fixes and the like, but I'm fairly certain getMousePosition() couldn't create any bugs(since it doesn't really touch anything outside of the function itself), adding it is a matter of dropping in something like 12 lines of code, and just that one function alone allows GUI coders to create labels that can be easily moved/resized via mouse(which is something I'd like to put in the hands of people like Zulah in Achaea's community as quick as I can). Is it something that could be considered for the next preview, or should I not expect to see it officially added until 3 is finalized?

I think I know enough about Github now to make my own fork, add the mouse position code, and submit a pull request.

Another question, would it be better to ask questions about how would be best to implement this on here, or on Gitter? The questions I have on this matter so far:

Is the name getMousePosition() keeping with your direction on naming these functions?

Is it preferable for it to return two values(so: x, y = getMousePosition()), or would it be better for some reason for it to return a table with the values in it(so: x, y = getMousePosition().x, getMousePosition().y)? I'm pretty sure the first way makes more sense, it's also how my implementation works at present.

User avatar
SlySven
Posts: 1019
Joined: Mon Mar 04, 2013 3:40 pm
Location: Deepest Wiltshire, UK
Discord: SlySven#2703

Re: Messing with userwindows

Post by SlySven »

...About 20 lines of code later, I have a label that allows me to move it by clicking it and then moving the mouse and clicking again when it's where you want it(since it appears there is no way to set a ClickRelease callback on labels?)...
When you say " there is no way to set a ClickRelease callback on labels" do you mean that we do not currently provide a means to do that OR that it is not possible to do on a general basis - I suspect the former and I'm sure that someone (I'm not volunteering at this time, BTW) can come up with something to do that in the future! :)
...adding it is a matter of dropping in something like 12 lines of code...
Don't forget suitable error handling/messages, ideally already configured for I18n - using tr(...).toUft8().constData() wrappers around outgoing strings to user/script environment and parsing incoming text strings from there with QString::fromUtf8(...). :geek:

Speaking personally, currently returning two values being the x and the y coordinates is the easiest to code but looking ahead I think it would be wisest to go for a two entry table where the first key, 1 is the x coordinate and the second 2 is y. This is because using alphabetical texts as keys may be awkward in the future for other languages - fair enough "x" and "y" are pretty much going to be standard across the Western Hemisphere at least, but, for example where we have hard-coded colour components as "r", "g", "b" and "a" doesn't work so well when moving away from English, e.g. French would want "r", "v", "b", "a"...!

Also, a table form of results would only be a single item to return from the C++ function to the Lua system (i.e. the former would end with return 1;) and that works better for the two return values: nil + error message that we try and (now) use for run-time error handling where the input is plausibly valid but doesn't work out (the user asks for the mouse position for a label with a string label name as an argument and the label doesn't exist) which is different from the case where the "type" of the supplied arguments is wrong (the user asks for the same thing but supplies a boolean instead of the string which is the label's name - unless they are accessed by a numeric Id, I'd have to check! :P ) which is when we'd raise/throw a Lua error which would abort the getMousePosition(...) without returning data.

dicene
Posts: 47
Joined: Thu Sep 25, 2014 7:36 am

Re: Messing with userwindows

Post by dicene »

Re: ClickRelease, I don't see any code to do it yet, but if I remember what the onClick code looks like, it should be pretty trivial to add. If that's the case, I'll do it myself. I'll try and remember to look at it when I get home.

As far as getMousePosition goes, I wasn't requiring any sort of arguments passed to the function. Due to the method I'm using to grab the mouse coordinates, it's just returning the x, y position relative to the window that the mouse is over(meaning if you mouse over a userwindow, it's pulling the relative x, y in that userwindow), but it doesn't care if you're over a label. I figured this was probably the simplest way to give access to that information without changing how an existing function works.

User avatar
Vadi
Posts: 5035
Joined: Sat Mar 14, 2009 3:13 pm

Re: Messing with userwindows

Post by Vadi »

dicene wrote:So, I saw you mention something about pushing out another preview in 3 weeks or so. I know that you're focusing on bug-fixes and the like, but I'm fairly certain getMousePosition() couldn't create any bugs(since it doesn't really touch anything outside of the function itself), adding it is a matter of dropping in something like 12 lines of code, and just that one function alone allows GUI coders to create labels that can be easily moved/resized via mouse(which is something I'd like to put in the hands of people like Zulah in Achaea's community as quick as I can). Is it something that could be considered for the next preview, or should I not expect to see it officially added until 3 is finalized?
There's a lot of seemingly easy fixes that could be made like this - each one requires not only testing, but also proper testing and documentation. The only work needed to finish 3.0 is to fix the bugs we've added to the client since 2.1, a simple enough goal - I'd like to focus on it. You are still welcome to submit a PR on Github because post-3.0 code is available in a separate branch, so it could just wait there until it's ready to come in.
dicene wrote: Another question, would it be better to ask questions about how would be best to implement this on here, or on Gitter? The questions I have on this matter so far:

Is the name getMousePosition() keeping with your direction on naming these functions?

Is it preferable for it to return two values(so: x, y = getMousePosition()), or would it be better for some reason for it to return a table with the values in it(so: x, y = getMousePosition().x, getMousePosition().y)? I'm pretty sure the first way makes more sense, it's also how my implementation works at present.
Anywhere works. Gitter would get more real-time responses from me, but forums make for better record-keeping.

The function should return x,y values - if the user wants it in a table, they can save the return values into it. Returning x,y will also be consistent with how other functions in the API work, such as getRoomCoordinates().

User avatar
Vadi
Posts: 5035
Joined: Sat Mar 14, 2009 3:13 pm

Re: Messing with userwindows

Post by Vadi »

SlySven wrote:Speaking personally, currently returning two values being the x and the y coordinates is the easiest to code but looking ahead I think it would be wisest to go for a two entry table where the first key, 1 is the x coordinate and the second 2 is y. This is because using alphabetical texts as keys may be awkward in the future for other languages - fair enough "x" and "y" are pretty much going to be standard across the Western Hemisphere at least, but, for example where we have hard-coded colour components as "r", "g", "b" and "a" doesn't work so well when moving away from English, e.g. French would want "r", "v", "b", "a"...!
Why would the French want v,b,a? It is the international standard to code in English. Chinese code in English, Russians code in English, everyone does. Plus, if they want to rename variables to what they'd like, returning values is the best way to go about it - since then we give the power to the user to name them whatever they'd like.

User avatar
Vadi
Posts: 5035
Joined: Sat Mar 14, 2009 3:13 pm

Re: Messing with userwindows

Post by Vadi »

dicene wrote: As far as getMousePosition goes, I wasn't requiring any sort of arguments passed to the function. Due to the method I'm using to grab the mouse coordinates, it's just returning the x, y position relative to the window that the mouse is over(meaning if you mouse over a userwindow, it's pulling the relative x, y in that userwindow), but it doesn't care if you're over a label. I figured this was probably the simplest way to give access to that information without changing how an existing function works.
I think you should add in the window name into there, else you won't know what window are you getting the values for - the main or a userwindow.

dicene
Posts: 47
Joined: Thu Sep 25, 2014 7:36 am

Re: Messing with userwindows

Post by dicene »

My employer made my decision on Mudlet-Forums v Gitter for me, since I don't have access to Gitter at work. :|

First, I played with getMousePosition a little to make sure I was right on how it works. I originally had it using a function that was returning mouse position relative to the window the mouse was over. I've since changed it so it just returns mouse position relative to the main console. It isn't limited by the bounds of the Mudlet window either. If you move your mouse to another monitor outside of the Mudlet window, you can still retrieve the position of the mouse accurately. At this point, I see no reason to change it so that you can specify a window, since I can think of no use-case where that functionality is necessary.

Here's what the working code looks like(pulling mouse position relative to the main window only)(I think the commented line is how I had it giving the mouse position relative to whatever window it was over):
Code: [show] | [select all] lua
int TLuaInterpreter::getMousePosition( lua_State *L )
{
    Host * pHost = TLuaInterpreter::luaInterpreterMap[L];
    QPoint pos = pHost->mpConsole->mapFromGlobal(QCursor::pos());
    //QPoint pos = QCursor::pos();

    lua_pushnumber( L, pos.x() );
    lua_pushnumber( L, pos.y() );

    return 2;
}
Second, re: MouseReleaseEvents. I basically just copied over all the setLabelClickCallback code and replaced "Click" with "Release". Seems to be working perfectly fine. Only thing I didn't implement and test was the geyser code to give labels an internal function(label.setClickCallback) so you don't have to rely on setLabelClickCallback(name, func, arg)). I'm a little unsure about what's going on in the constructor Geyser.Label:new.

Is the following code just setting the nested labels to have the same callbacks already assigned to their parents?
Code: [show] | [select all] lua
   
   -- Call parent's constructor
   local me = self.parent:new(cons, container)

   -- Set the metatable.
   setmetatable(me, self)
   self.__index = self
...
    if me.callback then
      if type(me.args) == "" then
         me:setClickCallback(me.callback, me.args)
      elseif type(me.args) == "table" then
         me:setClickCallback(me.callback, unpack(me.args))
      else
         me:setClickCallback(me.callback)
      end
   end

dicene
Posts: 47
Joined: Thu Sep 25, 2014 7:36 am

Re: Messing with userwindows

Post by dicene »

So, played around with getMousePosition and setLabelReleaseCallback some more and built a sort of basic framework that lets you create draggable/resizable windows that have the complicated parts already baked in.

Here is the framework:
Code: [show] | [select all] lua
--testDWin
dwins = dwins or {}
dwinFuncs = dwinFuncs or {}

function createDWin(args)
  local winName = args.name or "unknown"
  local x = args.x or 300
  local y = args.y or 200
  local width = args.width or 300
  local height = args.height or 200
  local borderWidth = args.borderWidth or 3
--  if dwins[winName] then return end
  local oldRef = dwins[winName] or {}
  dwins[winName] = {}
  dwins[winName].name = winName
  dwins[winName].x = oldRef.x or x
  dwins[winName].y = oldRef.y or y
  dwins[winName].width = oldRef.width or width
  dwins[winName].height = oldRef.height or height
  dwins[winName].borderWidth = borderWidth
  local bw = dwins[winName].borderWidth
  dwins[winName].border = Geyser.Label:new({
   name=winName .. "_border",
   x=dwins[winName].x, y=dwins[winName].y,
   width=dwins[winName].width, height=dwins[winName].height,
  })
  dwins[winName].title = Geyser.Label:new({
   name=winName .. "_title",
   x=dwins[winName].x+bw, y=dwins[winName].y+bw,
   width=dwins[winName].width-(bw*2), height=15,
  })
--  dwins[winName].main = Geyser.Label:new({
  dwins[winName].main = Geyser.MiniConsole:new({
   name=winName,
   x=dwins[winName].x+bw, y=dwins[winName].y+15+bw,
   width=dwins[winName].width-(bw*2), height=dwins[winName].height-15-(bw*2),
  })
  dwins[winName].border:setColor("|c999999")
  dwins[winName].title:setColor("|c668866")
  dwins[winName].main:setColor("|c111111")
  dwins[winName].titleClick = function ()
    dwins[winName].dragging = true
    dwinDragCheck(winName)
  end
--  local funcName = [[dwins["]]..winName..[["].titleClick]]
  dwins[winName].titleRelease = function ()
    dwins[winName].dragging = false
  end
  dwins[winName].borderClick = function ()
    local mx, my = getMousePosition()
    local edge = ""
    if my < dwins[winName].y + dwins[winName].borderWidth then
      edge = "north"
    elseif my >= dwins[winName].y + dwins[winName].height - dwins[winName].borderWidth then
      edge = "south"
    else
      edge = ""
    end
    if mx < dwins[winName].x + dwins[winName].borderWidth then
      edge = edge .. "west"
    elseif mx >= dwins[winName].x + dwins[winName].width - dwins[winName].borderWidth then
      edge = edge .. "east"
    else
      edge = edge .. ""
    end
--    echo(string.format("x: %i, y: %i == mx: %i, my: %i\n", dwins[winName].x, dwins[winName].y, mx, my))
--    echo("Dragging the " .. edge .. " edge.\n")
    dwins[winName].edgeDragging = true
    dwins[winName].edge = edge
    dwinEdgeDragCheck(winName)
  end
  dwins[winName].borderRelease = function ()
    dwins[winName].edgeDragging = false
    dwins[winName].edge = nil
  end
  dwins[winName].title:setClickCallback("dwins." .. winName .. ".titleClick")
  setLabelReleaseCallback(winName .. "_title", "dwins." .. winName .. ".titleRelease", winName .. "_title")
  dwins[winName].border:setClickCallback("dwins." .. winName .. ".borderClick")
  setLabelReleaseCallback(winName .. "_border", "dwins." .. winName .. ".borderRelease", winName .. "_border")
  return dwins[winName]
end

function dwinDragCheck(winName)
  if not dwins[winName].dragging then
    dwins[winName].mx = nil
    dwins[winName].my = nil
    return
  end
  local mx, my = getMousePosition()
  if dwins[winName].mx and dwins[winName].my and (dwins[winName].mx ~= mx or dwins[winName].my ~= my) then
    dwins[winName].x = dwins[winName].x + (mx-dwins[winName].mx)
    if dwins[winName].x < 0 then dwins[winName].x = 0 end
    dwins[winName].y = dwins[winName].y + (my-dwins[winName].my)
    if dwins[winName].y < 0 then dwins[winName].y = 0 end
    dwinRedraw(winName)
  end
  dwins[winName].mx, dwins[winName].my = mx, my

  tempTimer(0.03, [[dwinDragCheck("]] .. winName .. [[")]])
end

function dwinEdgeDragCheck(winName)
  if not dwins[winName].edgeDragging then
    dwins[winName].mx = nil
    dwins[winName].my = nil
    return
  end
  local mx, my = getMousePosition()
  if dwins[winName].mx and dwins[winName].my and (dwins[winName].mx ~= mx or dwins[winName].my ~= my) then
    if dwins[winName].x >= 0 and mx >= 0 then
      if dwins[winName].edge:match("west") then
        dwins[winName].x = dwins[winName].x + (mx-dwins[winName].mx)
        dwins[winName].width = dwins[winName].width - (mx-dwins[winName].mx)
      elseif dwins[winName].edge:match("east") then 
--        dwins[winName].x = dwins[winName].x - (mx-dwins[winName].mx)
        dwins[winName].width = dwins[winName].width + (mx-dwins[winName].mx)
      end
    end
    if dwins[winName].y >= 0 and my >= 0 then
      if dwins[winName].edge:match("north") then
        dwins[winName].y = dwins[winName].y + (my-dwins[winName].my)
        dwins[winName].height = dwins[winName].height - (my-dwins[winName].my)
      elseif dwins[winName].edge:match("south") then 
--        dwins[winName].y = dwins[winName].y + (my-dwins[winName].my)
        dwins[winName].height = dwins[winName].height + (my-dwins[winName].my)
      end
    end
    dwinRedraw(winName)
  end
  dwins[winName].mx, dwins[winName].my = mx, my

  tempTimer(0.03, [[dwinEdgeDragCheck("]] .. winName .. [[")]])
end

function dwinRedraw(winName)
  dwins[winName].border:move(dwins[winName].x, dwins[winName].y)
  dwins[winName].border:resize(dwins[winName].width, dwins[winName].height)
  dwins[winName].title:move(dwins[winName].x+dwins[winName].borderWidth, dwins[winName].y+dwins[winName].borderWidth)
  dwins[winName].title:resize(dwins[winName].width-(dwins[winName].borderWidth*2), dwins[winName].height-15)
  dwins[winName].main:move(dwins[winName].x+dwins[winName].borderWidth, dwins[winName].y+15+dwins[winName].borderWidth)
  dwins[winName].main:resize(dwins[winName].width-(dwins[winName].borderWidth*2), dwins[winName].height-15-(dwins[winName].borderWidth*2))
end

function hideDWin(winName)
  dwins[winName].border:hide()
  dwins[winName].title:hide()
  dwins[winName].main:hide()
end

function showDWin(winName)
  dwins[winName].border:show()
  dwins[winName].title:show()
  dwins[winName].main:show()
end
Here is the code that generated the windows in this example:
Code: [show] | [select all] lua
statBox = createDWin({name = "statBox", x = 300, y = 300, width = 300, height = 200, borderWidth = 10})
statBox.title:echo("Stats", nil, "cb")
statBox.main:cecho("<green>Blah!\nBlah!")

inventoryBox = createDWin({name = "inventoryBox", x = 700, y = 300, width = 300, height = 200, borderWidth = 2})
inventoryBox.title:echo("Inventory", nil, "cb")
inventoryBox.main:hecho("|cffaaffItem 1\n|caabb34Item 2\n|c5555ffItem 3")

testBox = createDWin({name = "testBox", x = 500, y = 300, width = 300, height = 200, borderWidth = 5})

dirBox = createDWin({name = "dirBox", x = 200, y = 200, width = 200, height = 150, borderWidth = 5})
A screenshot of an arrangement of those windows I did with nothing but the mouse(yes, the borderWidth of 10 on one of those is ugly):
Image

And here's a video of me showing off the drag-moving and drag-resizing properties of those windows:

https://vimeo.com/189117740

With a little bit of polish and some more optional initialization arguments like title color, title text, wrapping, possibly allowing anchoring of them to %'s instead of actual pixel numbers so you could set up windows that displayed properly at other resolutions(like when I unplug my monitor and change from 1920 x 1080 to 1366 x 780 or something), and other things about this system fleshed out, I'll likely start moving all my code into working with those windows. Fun. =D

User avatar
Vadi
Posts: 5035
Joined: Sat Mar 14, 2009 3:13 pm

Re: Messing with userwindows

Post by Vadi »

Looking good!

User avatar
SlySven
Posts: 1019
Joined: Mon Mar 04, 2013 3:40 pm
Location: Deepest Wiltshire, UK
Discord: SlySven#2703

Re: Messing with userwindows

Post by SlySven »

Do you worry about Z-ordering? I.e. whether one window/label is above or below another, I can't remember whether there is any logic/code that allows any sort of sensible control over that already...

Post Reply