New module to simplify moving miniwindows around by dragging them

Posted by Nick Gammon on Thu 16 Jul 2009 02:34 AM — 16 posts, 75,215 views.

Australia Forum Administrator #0
After the new functionality of dragging was introduced in MUSHclient version 4.40, I have been adding dragging functionality to some of my plugins. However getting it right is fiddlier than it looks for various reasons. Thus I have now made a separate module (which is distributed with MUSHclient version 4.42 onwards) that simplifies adding dragging. It only requires adding four lines to an existing plugin, as described in the comments at the start of the module.

As an example of how you might use the module, I'll show parts of my Aardwolf campaign tracker, adding in the bits to make the window moveable.


First, looking at OnPluginInstall, this is the original:


function OnPluginInstall ()
  
  win = GetPluginID ()
  font_id = "fn"
  
  font_name = "Comic Sans MS"    -- the actual font
  
  -- make window so I can grab the font info
  WindowCreate (win, 
                 0, 0, 1, 1,  
                 1,   -- irrelevant
                 0, 
                 background_colour) 

 WindowFont (win, font_id, font_name, 8, false, false, false, false, 0, 0)  -- normal  
 font_height = WindowFontInfo (win, font_id, 1)  -- height

end -- OnPluginInstall


Now we need to "require" the module, and tell it to get the position of the window from last time the plugin was used:


function OnPluginInstall ()
  
  win = GetPluginID ()
  font_id = "fn"
  
  font_name = "Comic Sans MS"    -- the actual font
  
  require "movewindow"  -- load the movewindow.lua module

  -- install the window movement handler, get back the window position
  windowinfo = movewindow.install (win, 7)  -- default to 7 (on right, center top/bottom)
   
  -- make window so I can grab the font info
  WindowCreate (win, 
                 windowinfo.window_left, 
                 windowinfo.window_top, 
                 1, 1,              -- width, height
                 windowinfo.window_mode,   
                 windowinfo.window_flags,
                 background_colour) 

  WindowFont (win, font_id, font_name, 8, false, false, false, false, 0, 0)  -- normal  
  font_height = WindowFontInfo (win, font_id, 1)  -- height
  
end -- OnPluginInstall


The call to movewindow.install prepares for window dragging, and returns in the windowinfo table the location the window was at last time, so we can plug these into the WindowCreate call.

The next thing to do is to save the window's position when the plugin saves its state. The original OnPluginSaveState function (which may not even exist) would look like this:


function OnPluginSaveState ()
end -- function OnPluginSaveState


So we need to add one line to remember the window position:


function OnPluginSaveState ()
  -- save window current location for next time  
  movewindow.save_state (win)
end -- function OnPluginSaveState


Finally we need to add a drag handler after deleting hotspots, or after creating or re-creating the window.

My old function which showed the list of mobs to kill was this:


function show_campaign_text ()

  -- do nothing if no campaign
  if #campaign_info == 0 or when_required == nil then
    return
  end -- if

  -- recreate the window the correct size
  WindowCreate (win, 
                 0, 0,   -- left, top (auto-positions)
                 max_width + 10,     -- width
                 (#campaign_info + 2)   * font_height + 5,  -- height
                 7,       -- auto-position: top middle
                 0,  -- flags
                 0xE7FFFF) 
 
  -- heading
  local text = "Current campaign. You need to kill:"
  max_width = math.max (max_width, WindowTextWidth (win, font_id, text))
  Display_Line (1, text, font_id, heading_colour)

  -- list of mobs
  for i, v in ipairs (campaign_info) do
    Display_Line (i + 1, v, font_id, text_colour)
  end -- for
  
  -- how long to go
  local time_to_go = when_required - os.time ()
  
  if time_to_go < 0 then
    return
  end -- if
  
  text = string.format ("Time to go: %s", convert_time (time_to_go))
  max_width = math.max (max_width, WindowTextWidth (win, font_id, text))
  Display_Line (#campaign_info + 2, text, font_id, time_colour)
    
  WindowShow (win, true)  
end -- show_campaign_text


Now instead of auto-positioning the window, we need to use the coordinates saved in the windowinfo table, like this:


function show_campaign_text ()

  -- do nothing if no campaign
  if #campaign_info == 0 or when_required == nil then
    return
  end -- if

  -- recreate the window the correct size and position
  WindowCreate (win, 
                 windowinfo.window_left, 
                 windowinfo.window_top, 
                 max_width + 10,     -- width
                 (#campaign_info + 2)   * font_height + 5,  -- height
                 windowinfo.window_mode,   -- whatever
                 windowinfo.window_flags,
                 0xE7FFFF) 
 
  -- draw drag bar rectangle
  WindowRectOp (win, 2, 0, 0, 0, font_height, 0x8CE6F0)
  
  -- heading
  local text = "Current campaign. You need to kill:"
  max_width = math.max (max_width, WindowTextWidth (win, font_id, text))
  Display_Line (1, text, font_id, heading_colour)

  -- add the drag handler so they can move the window around
  movewindow.add_drag_handler (win, 0, 0, 0, font_height)
  
  -- list of mobs
  for i, v in ipairs (campaign_info) do
    Display_Line (i + 1, v, font_id, text_colour)
  end -- for
  
  -- how long to go
  local time_to_go = when_required - os.time ()
  
  if time_to_go < 0 then
    return
  end -- if
  
  text = string.format ("Time to go: %s", convert_time (time_to_go))
  max_width = math.max (max_width, WindowTextWidth (win, font_id, text))
  Display_Line (#campaign_info + 2, text, font_id, time_colour)
    
  WindowShow (win, true)  
end -- show_campaign_text


I also added a WindowRectOp call to make the "title bar" a different colour, to make it more obvious this was where you clicked to drag. Finally the call to movewindow.add_drag_handler sets up the dragging hotspot. In this case I made it go from 0,0 (top left) to line down (vertically, font_height down).




One of the complexities in the movewindow module was that I wanted to support multiple windows in a single plugin. Thus each window is allocated its own table in the global namespace, based on the miniwindow ID. That way it is possible to have multiple windows and drag them around independently. An example of that might be an inventory window, with a separate window for the contents of your bags. This is why you have to pass down the miniwindow ID to the drag handler functions, so they can associate themselves with the correct window.
Amended on Thu 16 Jul 2009 02:40 AM by Nick Gammon
USA Global Moderator #1
I'd like to lobby for an amendment to .install that takes another parameter to optionally disable the call to
DoAfterSpecial (5, "mw_" .. win .. "_movewindow_info.check_map_position ()" , sendto.script)


I like the simplicity of the drag module, but that line seems to do more harm than good for me.
USA Global Moderator #2
Also, the ability to pass in a list of other miniwindows to be synchronously moved by the same amount would be awesome.
Australia Forum Administrator #3
Fiendish said:

I'd like to lobby for an amendment to .install that takes another parameter to optionally disable the call to
DoAfterSpecial (5, "mw_" .. win .. "_movewindow_info.check_map_position ()" , sendto.script)


I like the simplicity of the drag module, but that line seems to do more harm than good for me.


Added a parameter to do that. New commit 9e32881.

http://github.com/nickgammon/mushclient/blob/master/lua/movewindow.lua
USA Global Moderator #4
Oh for crying out loud...4th try at posting this. :)

Nick, I'd like your opinion on...

@@ -104,10 +104,17 @@
     
     -- find where window is in case we drag it offscreen
     mwi.origx = WindowInfo (win, 10) 
     mwi.origy = WindowInfo (win, 11)
     
+    -- find where the friends are relative to the window
+    for j,v in ipairs(mwi.window_friends) do
+        if (v ~= nil) then
+            mwi.window_friend_deltas[j] = {WindowInfo(v, 10) - mwi.origx, WindowInfo(v, 11) - mwi.origy}
+        end -- if
+    end -- for
+        
   end -- mousedown
 
 end -- make_mousedown_handler
 
 -- make a mouse drag-move handler with the movement information as an upvalue
@@ -123,10 +130,17 @@
                        WindowInfo (win, 18) - mwi.starty
   
     -- move the window to the new location - offset by how far mouse was into window
     WindowPosition(win, posx, posy, 0, 2);
     
+    -- move the friends if they still exist
+    for j,v in ipairs(mwi.window_friends) do
+        if (v ~= nil) then
+            WindowPosition(v, posx+mwi.window_friend_deltas[j][1], posy+mwi.window_friend_deltas[j][2], 0, 2)
+        end -- if
+    end -- for
+
     -- change the mouse cursor shape appropriately
     if posx < mwi.margin - WindowInfo (win, 3) or 
        posx > GetInfo (281) - mwi.margin or
        posy < 0 or   -- don't drag title out of view
        posy > GetInfo (280) - mwi.margin then
@@ -212,29 +226,33 @@
 end -- make_check_map_position_handler
 
 -- call movewindow.install in OnPluginInstall to find the position of the window, before creating it
 --  - it also creates the handler functions ready for use later
 
-function movewindow.install (win, default_position, default_flags, nocheck)
+function movewindow.install (win, default_position, default_flags, nocheck, default_friends)
 
   win = win or GetPluginID ()  -- default to current plugin ID
   
   assert (not string.match (win, "[^A-Za-z0-9_]"), "Invalid window name in movewindow.install: " .. win)
   
   default_position = default_position or 7 -- on right, center top/bottom
   default_flags = default_flags or 0
+  if (default_friends == nil) then
+    default_friends = {}
+  end
   
   -- set up handlers and where window should be shown (from saved state, if any)
   local movewindow_info = {
      win = win,   -- save window ID
      
      -- save current position in table (obtained from state file)
      window_left  = tonumber (GetVariable ("mw_" .. win .. "_windowx")) or 0,
      window_top   = tonumber (GetVariable ("mw_" .. win .. "_windowy")) or 0,
      window_mode  = tonumber (GetVariable ("mw_" .. win .. "_windowmode")) or default_position,
      window_flags = tonumber (GetVariable ("mw_" .. win .. "_windowflags")) or default_flags,
-     
+     window_friends = default_friends,
+     window_friend_deltas = {},
      margin = 20  -- how close we can put to the edge of the window
     }
 
     -- handler to reposition window
     movewindow_info.check_map_position = make_check_map_position_handler (movewindow_info)  -- for startup
Australia Forum Administrator #5
Looks OK. Personally I prefer:


 if v then


to:


 if (v ~= nil) then


Can you repost with forum codes escaped?

Template:post=9691
Please see the forum thread: http://gammon.com.au/forum/?id=9691.


If you don't there is a chance backslashes or things like [i] won't come out as you expect.
Australia Forum Administrator #6
And does the code seem to work?
USA Global Moderator #7
It seems to work. Except I changed.


WindowPosition(v, posx+mwi.window_friend_deltas[j][1], posy+mwi.window_friend_deltas[j][2], 0, 2)


to


WindowPosition(v, posx+mwi.window_friend_deltas[j][1], posy+mwi.window_friend_deltas[j][2], 0, WindowInfo(v, 8))
Australia Forum Administrator #8
OK can you repost with the square brackets escaped as I described (use MUSHclient to do that) and I'll merge it in.
USA Global Moderator #9
The whole module or just the diff?
USA Global Moderator #10
Here, how about this. :)

http://ilikepants.pastebin.com/deyK9YXn
Australia Forum Administrator #11
My diffs aren't working properly.

Can you post the diffs, but before putting them into the code tags, just go to MUSHclient's Edit menu -> Convert Clipboard Forum Codes. That will fix up the clipboard to be correct to paste here.
Amended on Wed 07 Apr 2010 02:57 AM by Nick Gammon
USA Global Moderator #12

@@ -104,10 +104,17 @@
     
     -- find where window is in case we drag it offscreen
     mwi.origx = WindowInfo (win, 10) 
     mwi.origy = WindowInfo (win, 11)
     
+    -- find where the friends are relative to the window
+    for i,v in ipairs(mwi.window_friends) do
+        if (v ~= nil) then
+            mwi.window_friend_deltas[i] = {WindowInfo(v, 10) - mwi.origx, WindowInfo(v, 11) - mwi.origy}
+        end -- if
+    end -- for
+        
   end -- mousedown
 
 end -- make_mousedown_handler
 
 -- make a mouse drag-move handler with the movement information as an upvalue
@@ -123,10 +130,17 @@
                        WindowInfo (win, 18) - mwi.starty
   
     -- move the window to the new location - offset by how far mouse was into window
     WindowPosition(win, posx, posy, 0, 2);
     
+    -- move the friends if they still exist
+    for i,v in ipairs(mwi.window_friends) do
+        if (v ~= nil) then
+            WindowPosition(v, posx+mwi.window_friend_deltas[i][1], posy+mwi.window_friend_deltas[i][2], 0, WindowInfo(v, 8))
+        end -- if
+    end -- for
+
     -- change the mouse cursor shape appropriately
     if posx < mwi.margin - WindowInfo (win, 3) or 
        posx > GetInfo (281) - mwi.margin or
        posy < 0 or   -- don't drag title out of view
        posy > GetInfo (280) - mwi.margin then
@@ -212,29 +226,33 @@
 end -- make_check_map_position_handler
 
 -- call movewindow.install in OnPluginInstall to find the position of the window, before creating it
 --  - it also creates the handler functions ready for use later
 
-function movewindow.install (win, default_position, default_flags, nocheck)
+function movewindow.install (win, default_position, default_flags, nocheck, default_friends)
 
   win = win or GetPluginID ()  -- default to current plugin ID
   
   assert (not string.match (win, "[^A-Za-z0-9_]"), "Invalid window name in movewindow.install: " .. win)
   
   default_position = default_position or 7 -- on right, center top/bottom
   default_flags = default_flags or 0
+  if (default_friends == nil) then
+    default_friends = {}
+  end
   
   -- set up handlers and where window should be shown (from saved state, if any)
   local movewindow_info = {
      win = win,   -- save window ID
      
      -- save current position in table (obtained from state file)
      window_left  = tonumber (GetVariable ("mw_" .. win .. "_windowx")) or 0,
      window_top   = tonumber (GetVariable ("mw_" .. win .. "_windowy")) or 0,
      window_mode  = tonumber (GetVariable ("mw_" .. win .. "_windowmode")) or default_position,
      window_flags = tonumber (GetVariable ("mw_" .. win .. "_windowflags")) or default_flags,
-     
+     window_friends = default_friends,
+     window_friend_deltas = {},
      margin = 20  -- how close we can put to the edge of the window
     }
 
     -- handler to reposition window
     movewindow_info.check_map_position = make_check_map_position_handler (movewindow_info)  -- for startup
Australia Forum Administrator #13
Added to next release, commit e3bebba.
#14
I'm at a complete loss as to how to use this to move a miniwindow. Well where to put any of the code really. I'm just getting into mini-windows and some of this just looks...well hard. Could someone do youtoube video on how to set this up beginning to end with the inventory mini-window thread? I just got that one down, so a demonstration would go a long way for helping me :)
Australia Forum Administrator #15
See http://www.gammon.com.au/forum/?id=9965&page=2 in which I added the dragging code to the inventory alias (it got turned into a plugin so that the dragging would work properly).