Panorama X has no object spacing feature

The ability to evenly space objects vertically or horizontally on a form was a useful feature of Panorama 6.0 which is missing from Panorama X. Is it likely to be implemented at any time?

I can write a custom statement to do it but I’m not going to bother if its return is imminent.

It is likely to be implemented at some time, but not imminent. When it is implemented, I would most likely do it using Panorama’s programming language, I don’t think Objective-C is required for this. If someone wanted to contribute a custom statement, I would certainly consider incorporating it into Panorama’s user interface.

I’ll have a go at it.

Here’s a first stab at it. About 50 lines of code and it seems to work OK except for the case where two or more objects initially coincide - I’m still working on that but, in the interim, you guys can see if you can break it or improve it. Dave will probably get it down to 15 lines :slight_smile:

As a temporary measure, I put the procedure in my home folder and, in the test databases, defined a hot key,

definehotkeys "global", "command-option-p", {openfile "~/ObjectSpacer"}

That’s all you have to do - the command opens the ObjectSpacer database which displays the Spacing dialog pirated from Panorama 6.0. There are other ways to do it and I’d welcome any suggestions.

A subsequent improvement would be for the dialog to open centred on the target form but I haven’t worked out how to do this without having an extra procedure.

Download the code from:

https://www.dropbox.com/s/374iwzv3m11qncy/ObjectSpacer.pandb.zip?dl=0

Great Work, Michael. It works even when the form is in Data mode.
But I think the “global” efficiency of your code could possibly be dangerous and lead to unexpected results, because the object spacing affects all objects in the frontmost form.
I would like it better if it would affect selected objects in Graphic Mode only.


It is not clear to me in which order the objects are spaced. In my test it was not according to the order in which I had created 1) a rectangle (orange), 2) a rounded rectangle (yellow) and 3) an oval (red). The object werde not spaced according to the layer order of the objects, either. So I was not able to predict the results.

Nice start Michael. Here are some of my thoughts on the project.

  1. If implemented natively in Pan X it will be available from the Objects menu if a form is in graphics mode.

  2. I think a dialog drop-down sheet (opendialogsheet) would be the best implementation.

  3. Like in Pan6 it would be most logical to start with the selected object that is closest to the top of the form for vertical spacing and work down in positional order. Likewise, you would start with the furthest left selected object on the form when doing the horizontal spacing.

Here is what the dialog sheet approach would look like (notice I have redrawn the form objects to look more Panorama X like):

I already told Michael that I would take care of that part. It would be quite difficult for anyone other than me to add items to the Object menu, though perhaps not impossible.

That said, let me provide some feedback. As mentioned by Kurt and Gary, this absolutely has to work on selected objects only. That should have been the starting point – I think you’ll have to rework the whole thing to do that.

You don’t want to use blueprints to examine the form objects. That’s double slow – first in creating the blueprints, then in your parsing of them. Also, the blueprints don’t tell you what objects are selected. Instead, use the objectinfoarray( function. Use the formula to generate the data you need in a format you can access quickly, perhaps tab delimited, with only the data you need. Use the query option to include only selected objects.

To quickly sort the objects by position, I would make either the top edge or left edge (depending on the direction) the first item in the array generated by objectinfoarray(. You’ll need to adjust this to add leading zeros, so that you can then use arraysort( to sort it more quickly than using arraymultisort(.

Actually, I think you only need the position and the object id in the generated array. Then you can use one loop to both calculate the new positions and change the object – no need for two loops. Since you’ll have the object id handy, you can use changobject instead of selectobjects and changeobjects. Also, I think rectangletweak( could be handy for calculating the new positions.

For anyone that hasn’t downloaded Michael’s code, I’m including it below to make it easier to follow my points.

;        .ObjectSpacer        14 August 2018

;  A procedure to evenly space, either vertically or horizontally, every object in a form.  Ideally, it might operate on a subset of all the objects but it's not there yet.

local mdI, mdJ, mdElement, mdIndex, mdNewTop
global mdBlue, mdBlueNew, mdDir, mdPoints

;  Close the ObjectSpacer file (which has stored the relocation parameters in three global variables), leaving the target form as the active window
;  Extract the location and size of every object in the target form and sort the locations on their 'rtop' value

closefile

formblueprint "",info(“formname”),mdBlue

arrayfilter mdBlue,mdBlue,lf(),?(import() contains "rectanglesize(" and import() notcontains "setwindowrectangle",import(),"") 
arraystrip mdBlue,lf()

arrayfilter mdBlue,mdBlue,lf(),tagdata(import(),"(",")",1)
mdBlue = ?(mdDir = "Vertical",arraymultisort(mdBlue,lf(),",","1r"),arraymultisort(mdBlue,lf(),",","2r"))

;  Check for less than two objects selected

mdJ = arraysize(mdBlue,lf())
if mdJ < 2
    returnerror "I need more objects to do this."
endif

;  Compute the new rtop or rleft values for the objects

looparray mdBlue,lf(),mdElement,mdIndex
    mdI = ?(mdDir = "Vertical",array(mdElement,1,","),array(mdElement,2,","))
    if mdIndex = 1
        mdBlueNew = mdI +","+ mdI
        mdNewTop = val(mdI) + ?(mdDir = "Vertical",val(array(mdElement,3,",")),val(array(mdElement,4,","))) + val(mdPoints)
    else
        mdBlueNew = mdBlueNew +lf()+ mdI +","+ mdNewTop 
        mdNewTop = mdNewTop + ?(mdDir = "Vertical",val(array(mdElement,3,",")),val(array(mdElement,4,","))) + val(mdPoints)
    endif
endloop

;  Apply the new rtop or rleft values to the objects

looparray mdBlueNew,lf(),mdElement,mdIndex
    mdI = val(array(mdElement,1,","))
    mdJ = val(array(mdElement,2,",")) - val(mdI)
    
    if mdDir = "Vertical"
        selectobjects rtop(objectinfo("rectangle")) = mdI
        changeobjects "rectangle",rectangleoffset(objectinfo("rectangle"),mdJ,0)
    else
        selectobjects rleft(objectinfo("rectangle")) = mdI
        changeobjects "rectangle",rectangleoffset(objectinfo("rectangle"),0,mdJ)
    endif
endloop

undefine mdI, mdJ, mdElement, mdIndex, mdNewTop, mdBlue, mdBlueNew, mdDir, mdPoints

Ok, I’m going to hide some other points way down here

  • I don’t think global variables should be used at all.
  • The inputs, direction and points, should be parameterized
  • Why are the buttons in the dialog created with shapes and text instead of using button objects??!!!
  • Why is there a hard coded “focus ring” around the text editor?
  • The dialog isn’t really a dialog. Hmm – I think the background is a screen shot of the Panorama 6 dialog!!

This was really a personal exercise to see what I could do at short notice. It operates on all objects and holds the topmost or leftmost one stationary.

Jim has given me more information to work on.

Reworking it isn’t a problem - it’s currently our wet season and I can’t get into the paddocks anyway. I’ll try the objectinfoarray( function as you suggest.

It was a quick first attempt Jim, not a Nobel Prize entry.

Being able to use Panorama X now because it’s raining seems to be a theme in the last couple of days! Send some of the rain here to California – we need it to fight fires! :rainbow:

Here’s a new version.

I’ve satisfied most of the suggestions and criticisms (all of which were useful). And ‘reworking the whole thing’ involved replacing four lines of code with one. The procedure is now down to 25 lines of code and comes with extensive internal documentation.

There are no global variables, the inputs are passed as parameters, the procedure works from the graphics mode of a database, only selected objects are spaced and I’ve used the objectinfoarray( function instead of blueprints.

I’ve retained the use of the multisort( function - it’s by far the easiest way to sort on the second column of a two-dimensional array. I’ve retained the copy of the Panorama 6.0 dialog box with added overlays and the procedure still resides in my Home folder, accessed by a hotkey. There’s no focus ring on the Space Between Objects button because of a Panorama X bug.

Jim will probably want to design his own modal dialog for inclusion in the Objects menu but I’m happy to run one up if he wants.

Good job.

I really don’t like using arraymultisort(. If you look inside that function, it’s doing a lot of extra logic that is going to slow things down. The speed may or may not matter, but it should be easy to eliminate the need for this.

Instead of:

mdBlue = objectinfoarray(
    {rectanglesizestr(integralrectangle(objectinfo("rectangle"))) +
    ","+objectinfo("id")}, {objectinfo("selected")},cr())

Try something like this:

mdBlue = objectinfoarray(
    {pattern(rtop(integralrectangle(objectinfo("rectangle"))),"#####") +
    ","+objectinfo("id")}, {objectinfo("selected")},cr())

This will result in an array something like this, with just a dimension (top, in this case) and an object id on each line:

00012,4
00145,7
00087,8

Of course, that will only work for vertical distribution, but it can be modified for both directions, and the sort can be built into the same line!

mdBlue = arraysort(objectinfoarray(
    {pattern(}+?(mdDir = "Vertical",{rtop(},{rleft(})+{integralrectangle(objectinfo("rectangle"))),"#####") +
    ","+objectinfo("id")}, {objectinfo("selected")},cr()))

Ok, my suggested changes above are going to screw up the way you are calculating the positions. You could add the rectanglesizestr( into the array again, but I think it would be easier and cleaner to just get the object position again in the loop.

looparray mdBlue,cr(),mdElement,mdElementNumber
    mdI = val(array(mdElement,2,","))
    let orect = objectinfo("rectangle", mdI)
    if mdElementNumber>1
        orect = rectangletweak(orect,?(mdDir = "Vertical","y","x"),farthestEdge+mdPoints
        changeobject mdI,"rectangle",orect
    endif
    let farthestEdge = ?(mdDir = "Vertical",rbottom(orect),rright(orect))
endloop

Note that the object selection isn’t needed at all, these lines can be removed:

selectobjects objectinfo("id") = mdI

selectnoobjects

That’s a good thing because the user isn’t going to like it if we change the selection.

Oh, one last thing, this should be added before the loop, so that Undo will work.

startgraphicschange "Fixed Object Spacing"

I didn’t test any of this, there is a good chance of bugs, but it should be pretty close. I think there is a good chance this can get put into version 10.1.1.

Maybe I’m wedded to it because I wrote it :wink:

I’ll check out your suggestions, which are actually more in the nature of advisories given that you have final say. I shouldn’t really be spending so much time on this - I have three other clients with major projects baying for my attention - but it’s been an interesting exercise.

This could be the end of it. I’ve taken a completely different approach to the handling of the array of coordinates so that there is now only one test for the spacing direction. The user’s initial object selection set is restored when finished. The user can specify minute spacings, like 0.0001 points. It’s still reasonably compact - only 28 lines of code. Some of them are complex but, given the array sizes involved, the execution time will be microseconds at worst.

When two or more objects have the same initial rtop( or rleft( values, they will be spaced in order of their ID numbers. I don’t see any other logical way to do this.

On the topic of speed of execution, rarely will a user be spacing more than a dozen or so objects so efficiency of code becomes more a matter of elegance.

In the dialog box, do we really need images to show users the difference between vertical and horizontal?

I get an error message when I set the distance to 0 point.

Yes - a zero distance is considered to be a non-entry. You could enter 0.001 but, on reflection, zero should be acceptable.

Anyway, I need another rewrite - I didn’t get to it last time until late at night and I wanted to get it finished before I went to bed - always a mistake - it has some utterly ridiculous code in it (I think Jim was too polite to point that out).

I’ll post a new version shortly.

1 Like

What you had last night was close enough, I’ve already started to integrate it into Panorama. I have cleaned it up a bit, here is the finished result.

let oSpace = 4
let spaceDirection = "Vertical"
let vSpacing = (spaceDirection beginswith "V")

let oList = arraysort(
        objectinfoarray(
            {pattern(}+?(vSpacing,{rtop(},{rleft(})+{integralrectangle(objectinfo("rectangle"))),"#####") +
            ","+objectinfo("id")}, {objectinfo("selected")},cr()),cr())

if linecount(oList)<2
    returnerror "Adjusting object spacing requires at least two selected objects."
endif

startgraphicschange "Adjust Object Spacing"
looparray oList,cr(),oListItem,oListNumber
    let oid = val(array(oListItem,2,","))
    let orect = objectinfo("rectangle",oid)
    if oListNumber>1
        orect = rectangletweak(orect,?(vSpacing,"y","x"),farthestEdge+oSpace)
        changeobject oid,"rectangle",orect
    endif
    let farthestEdge = ?(vSpacing,rbottom(orect),rright(orect))
endloop

I’ve got some ideas on how to extend this so that it will work with a grid of objects, not just a row or a column. But this code has been tested and does work.

1 Like

That’s excellent. In the interim, Kurt sent me a very nice reworked dialog box which I’ve sent to you off-list.

Hey Kurt, that is very clever how you managed to make that graphic entirely using Panorama’s built in tools! I thought it wasn’t possible because of the arrows, but you didn’t let that stop you! Perfect timing too, I was just about to work on the dialog.

Ok, it’s done. Here is a movie showing this new feature in action.

In most cases it is able to automatically set up the correct direction for you. In this example, the spacing is horizontal, and that is already set for you.

The algorithm has been enhanced so that it will work with a two dimensional grid of objects. I think this is pretty darn cool.

I took the movies above from the new documentation page for this feature.

Here is the final code. Getting it to work with a two dimensional grid added quite a bit of complexity.

let oSpace = val(parameter(1))
let spaceDirection = catcherror("Vertical",parameter(2))
let vSpacing = (spaceDirection beginswith "V")

let oList = arraysort(
        objectinfoarray(
            {pattern(}+?(vSpacing,{rtop(},{rleft(})+{integralrectangle(objectinfo("rectangle"))),"#####") +
            ","+objectinfo("id")}, {objectinfo("selected")},cr()),cr())

if linecount(oList)<2
    returnerror "Adjusting object spacing requires at least two selected objects."
endif

startgraphicschange "Adjust Object Spacing"
let chunkStartEdge = -1
looparray oList,cr(),oListItem,oListNumber
    let oid = val(array(oListItem,2,","))
    let orect = objectinfo("rectangle",oid)
    if oListNumber>1
        let newEdge = farthestEdge+oSpace
        let newStartEdge = ?(vSpacing,rtop(orect),rleft(orect))
        if newStartEdge = chunkStartEdge
            // continuing the same row or column
            newEdge = chunkNewEdge
        else
            // starting a new row or column
            chunkStartEdge = newStartEdge
        endif
        orect = rectangletweak(orect,?(vSpacing,"y","x"),newEdge)
        let chunkNewEdge = newEdge
        changeobject oid,"rectangle",orect
    else
        chunkStartEdge = ?(vSpacing,rtop(orect),rleft(orect))
        let chunkNewEdge = chunkStartEdge
    endif
    let farthestEdge = ?(vSpacing,rbottom(orect),rright(orect))
endloop

The dialog itself is in a separate procedure. The first four lines are used to decide whether the default should be Vertical or Horizontal.

let topEdges = arraydeduplicate(objectinfoarray({int(rtop(objectinfo("rectangle")))},{objectinfo("selected")},cr()),cr())
let leftEdges = arraydeduplicate(objectinfoarray({int(rleft(objectinfo("rectangle")))},{objectinfo("selected")},cr()),cr())
let topEdgesCount = linecount(topEdges)
let leftEdgesCount = linecount(leftEdges)

loop
    rundialog |||
        Form="Adjust Object Spacing"
        File="_FormLib"
        sheet=true
        Menus=normal
        OkButton="Update Spacing"
        Height=300 Width=333
    |||
    stoploopif info("trigger")="Dialog.Close"
    if info("trigger") = "Dialog.Initialize"
        letfileglobal objectSpacing = catcherror("1",getpreferencevalue("PXObjectSpacingGap"))
        letfileglobal objectSpacingDirection = ?(topEdgesCount>=leftEdgesCount,"Vertical","Horizontal")
        showvariables objectSpacing,objectSpacingDirection
        objectaction "Space","Open"
    endif
endloop

if dlgResult="Cancel"
    return
endif

if dlgResult="Ok"
    let objectSpacingGap = fileglobalvalue("_FormLib","objectSpacing")
    setpreferencevalues "PXObjectSpacingGap",objectSpacingGap
    adjustobjectspacing objectSpacingGap,fileglobalvalue("_FormLib","objectSpacingDirection")
endif

Other than this there is also a bit of Objective-C code to add this to the menus. But that is only about a dozen lines. It does dim the menu item if less than two objects are selected.

Although the code is quite different from what was discussed here yesterday, the code that was submitted did help quite a lot. Plus it motivated me to do this now. This was kind of fun and I’m quite pleased with how it turned out, on the other hand I kind of think I should have spent that time today working on Panorama Server. And as you can see, there is a lot more to getting a feature out than just writing the code, it also has to be documented, etc. This “easy little project” pretty much took up all my development time for a day (not to mention the contributions from Michael and Kurt, thank you). Oh well, I know at least three of you will like this a lot, and probably more.