Author Topic: PJSR: 1171 Dialog layout issues  (Read 6765 times)

Offline mschuster

  • PTeam Member
  • PixInsight Jedi
  • *****
  • Posts: 1087
PJSR: 1171 Dialog layout issues
« on: 2015 August 22 17:40:11 »
Hi Juan,

Some 1171 dialog layout issues. I will add posts to this thread with other similar issues. Priority low.

Mike

1171 Win7 UI scaling 1.4, also at UI scaling 1.0.
Clipping of Label and Edit items. Position of GroupBox title. Click through the tabs to see other layouts.

https://dl.dropboxusercontent.com/u/109232477/PixInsight/Layout/TabGroupBox1.js

Offline mschuster

  • PTeam Member
  • PixInsight Jedi
  • *****
  • Posts: 1087
Re: PJSR: 1171 Dialog layout issues
« Reply #1 on: 2015 August 22 22:25:09 »
1171 Win7 UI scaling 1.4, also at UI scaling 1.0.
Overlap of PushButtons and TreeBox. GroupBox title position again.

https://dl.dropboxusercontent.com/u/109232477/PixInsight/Layout/TabGroupBox2.js

Offline mschuster

  • PTeam Member
  • PixInsight Jedi
  • *****
  • Posts: 1087
Re: PJSR: 1171 Dialog layout issues
« Reply #2 on: 2015 August 23 13:04:46 »
1171 Win7 UI scaling 1.4, also at UI scaling 1.0.
ComboBox control and menu item list vertical layouts.

https://dl.dropboxusercontent.com/u/109232477/PixInsight/Layout/TabGroupBox3.js

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Re: PJSR: 1171 Dialog layout issues
« Reply #3 on: 2015 September 08 16:16:27 »
Hi Mike,

The ComboBox problem is a Qt bug, which continues unresolved for a long time. I have found this thread:

http://stackoverflow.com/questions/13308341/qcombobox-abstractitemviewitem

I have applied the solution given in that thread and it works perfectly (with a few modifications), so this bug is now fixed, and ComboBox controls are now rendered nicely.

The bug with GroupBox titles is now also fixed on all platforms. I am working on the Edit clipping issue right now, so count on it fixed in the next version.

Thank you for reporting these problems.
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline Andres.Pozo

  • PTeam Member
  • PixInsight Padawan
  • ****
  • Posts: 927
Re: PJSR: 1171 Dialog layout issues
« Reply #4 on: 2015 September 09 01:36:15 »
Hi Juan,

could you give some guidelines about how to write dialogs which work well when the "UI scaling factor" is not 1?. I am using the new functions setScaledXXX but the results are not consistent. I use the expression var labelWidth = this.font.width("XXXXX"); for getting the size of the labels, but this value doesn't seem to work well when the scaling >1.

Offline mschuster

  • PTeam Member
  • PixInsight Jedi
  • *****
  • Posts: 1087
Re: PJSR: 1171 Dialog layout issues
« Reply #5 on: 2015 September 09 07:09:04 »
Andres, by trial and error on win7 this.font.width usually works for me but due to significant font metric changes I had to change the "xxxxx" string sometimes (usually?). This value works for Label.setFixedWidth(labelWidth), but not for spacing Sizers, where I use Sizer.addUnscaledSpacing(labelWidth). I also find it helpful to be able to run the script in old versions, so I am including code like below for the scaling methods I have needed so far. Sometimes I use the UI ratio directly (sizing high res Bitmaps and scaling graphics and fonts to use in them, etc.).

Mike

Code: [Select]
// Dynamic methods for core Control object.
#iflt __PI_BUILD__ 1168
if (!Control.prototype.displayPixelRatio) {
   Control.prototype.displayPixelRatio = 1;
}
#endif

if (!Control.prototype.setScaledFixedSize) {
   Control.prototype.setScaledFixedSize = function(w, h) {
      this.setFixedSize(w, h);
   };
}

if (!Control.prototype.setScaledMinSize) {
   Control.prototype.setScaledMinSize = function(w, h) {
      this.setMinSize(w, h);
   };
}

if (!Control.prototype.scaledResource) {
   Control.prototype.scaledResource = function(r) {
      return r;
   };
}

// Dynamic methods for core Sizer object.
if (!Sizer.prototype.addUnscaledSpacing) {
   Sizer.prototype.addUnscaledSpacing = function(s) {
      this.addSpacing(s);
   };
}
« Last Edit: 2015 September 09 14:02:11 by mschuster »

Offline mschuster

  • PTeam Member
  • PixInsight Jedi
  • *****
  • Posts: 1087
Re: PJSR: 1171 Dialog layout issues
« Reply #6 on: 2015 October 09 10:06:21 »
Thanks Juan,

The layouts for these three scripts is better in 1185 Win7, but still not correct IMO.

To see this, run the scripts and click between Tab1 and Tab2 repeatedly. The residual "squished" Tab1 layouts will be visible.

Also, the GroupBox title is still not positioned properly. It remains "too high" and rather than reasonably centered along the frame line as in 1123.

IMO the GroupBox control is not mediating layout constraints properly between its children and parent.

Thanks,
Mike
« Last Edit: 2015 October 09 10:16:47 by mschuster »

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Re: PJSR: 1171 Dialog layout issues
« Reply #7 on: 2015 October 09 14:44:55 »
GroupBox has always been problematic layout wise, especially when included in complex widgets with nested child layout levels, such as TabBox. All of these problems reflect bugs in their Qt counterpart classes (QGroupBox and QTabWidget, respectively), along with platform idiosyncrasies.

Try this version of your script:

Code: [Select]
// Win7 1123: Dialog.adjustToContents Edit vertical layout issue
// Win7 1171: Dialog.adjustToContents Edit vertical layout issue
// Win7 1171: GroupBox title vertical layout issue
// Win7 1171: ComboBox menu item layout issue

#include <pjsr/ColorSpace.jsh>
#include <pjsr/DataType.jsh>
#include <pjsr/FontFamily.jsh>
#include <pjsr/FrameStyle.jsh>
#include <pjsr/PenStyle.jsh>
#include <pjsr/SampleType.jsh>
#include <pjsr/Sizer.jsh>
#include <pjsr/StdButton.jsh>
#include <pjsr/StdIcon.jsh>
#include <pjsr/TextAlign.jsh>
#include <pjsr/UndoFlag.jsh>

#define TITLE "TabGroupBox3"

if (!Control.prototype.setScaledMinWidth) {
   Control.prototype.setScaledMinWidth = function(w) {
      this.setMinWidth(w);
   };
}

if (!Control.prototype.setScaledMinSize) {
   Control.prototype.setScaledMinSize = function(w, h) {
      this.setMinSize(w, h);
   };
}

function Tab1View(parent) {
   this.__base__ = Frame;
   this.__base__(parent);

   this.addGroupBox = function(title) {
      var groupBox = new GroupBox(this);
      this.sizer.add(groupBox);

      groupBox.sizer = new VerticalSizer;
      groupBox.sizer.margin = 6;
      groupBox.sizer.spacing = 6;
      groupBox.title = title;
      groupBox.styleSheet = "{}"; /* ### */
      return groupBox;
   };

   this.addPane = function(group) {
      var buttonPane = new HorizontalSizer;
      buttonPane.spacing = 6;
      group.sizer.add(buttonPane);

      return buttonPane;
   };

   this.addTreeBox = function(group, rows, paths, fullPaths) {
      var treeBox = new TreeBox(this);
      group.sizer.add(treeBox);

      treeBox.alternateRowColor = true;
      treeBox.headerVisible = false;
      treeBox.horizontalScrollBarVisible = true;
      treeBox.indentSize = 0;
      treeBox.multipleSelection = true;
      treeBox.numberOfColumns = 2;
      treeBox.setHeaderAlignment(0, TextAlign_Left | TextAlign_VertCenter);
      //console.writeln("lineSpacing: ", treeBox.font.lineSpacing);
      //console.writeln("borderWidth: ", treeBox.borderWidth);
      treeBox.setFixedHeight(
         this.displayPixelRatio * (rows + 1) * (treeBox.font.lineSpacing + 6) +
         treeBox.borderWidth
      );
      treeBox.showColumn(1, false);

      for (var i = 0; i < paths.length; ++i) {
         var node = new TreeBoxNode(treeBox);
         node.selectable = rows != 1;
         node.setText(0, fullPaths ? paths[i] : File.extractNameAndExtension(paths[i]));
      }
      treeBox.adjustColumnWidthToContents(0);

      return treeBox;
   };

   this.addPushButton = function(pane, text, toolTip, onClick) {
      var pushButton = new PushButton(this);
      pane.add(pushButton);

      pushButton.text = text;
      pushButton.toolTip = toolTip;
      pushButton.onClick = onClick;

      return pushButton;
   };

   this.addLabel = function(pane, text, toolTip) {
      var label = new Label(this);
      pane.add(label);

      label.setFixedWidth(this.labelWidth);
      label.text = text;
      label.toolTip = toolTip;
      label.textAlignment = TextAlign_Right | TextAlign_VertCenter;

      return label;
   };

   this.addEdit = function(pane, text, toolTip, onTextUpdated, onEditCompleted) {
      var edit = new Edit(this);
      pane.add(edit);

      edit.setFixedWidth(this.editWidth);
      edit.text = text;
      edit.toolTip = toolTip;
      edit.onTextUpdated = onTextUpdated;
      edit.onEditCompleted = onEditCompleted;

      return edit;
   };

   this.addUnit = function(pane, text) {
      var label = new Label(this);
      pane.add(label);

      label.setFixedWidth(this.unitWidth);
      label.text = text;
      label.textAlignment = TextAlign_Left | TextAlign_VertCenter;

      return label;
   };

   this.addComboBox = function(pane, items, currentItem, toolTip, onItemSelected) {
      var comboBox = new ComboBox(this);
      pane.add(comboBox);

      for (var i = 0; i != items.length; ++i) {
         comboBox.addItem(items[i]);
      }
      comboBox.currentItem = currentItem;
      comboBox.toolTip = toolTip;
      comboBox.onItemSelected = onItemSelected;
      return comboBox;
   };

   this.sizer = new VerticalSizer();
   this.sizer.margin = 6;
   this.sizer.spacing = 6;

   this.labelWidth = this.parent.font.width("Label 0:");
   this.editWidth = this.parent.font.width("00000000000");
   this.unitWidth = this.parent.font.width("unit");

   {
      this.group1Box = this.addGroupBox("Group1:");

      this.button1Pane = this.addPane(this.group1Box);

      this.observationWavelengthTypicalValuesComboBox = this.addComboBox(
         this.button1Pane,
         ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta", "Eta", "Theta"],
         0,
         "<p></p>",
         function(item) {}
      );

      this.button1Pane.addStretch();
   }

   {
      this.group1Box = this.addGroupBox("Group2:");
   }

   {
      this.group1Box = this.addGroupBox("Group3:");
   }

   this.sizer.addStretch();
}
Tab1View.prototype = new Frame;

function Tab2View(parent) {
   this.__base__ = Frame;
   this.__base__(parent);

   this.addGroupBox = function(title) {
      var groupBox = new GroupBox(this);
      this.sizer.add(groupBox);

      groupBox.sizer = new VerticalSizer;
      groupBox.sizer.margin = 6;
      groupBox.sizer.spacing = 6;
      groupBox.title = title;
      groupBox.styleSheet = "{}"; /* ### */
      return groupBox;
   };

   this.addPane = function(group) {
      var buttonPane = new HorizontalSizer;
      buttonPane.spacing = 6;
      group.sizer.add(buttonPane);

      return buttonPane;
   };

   this.addTreeBox = function(group, rows, paths, fullPaths) {
      var treeBox = new TreeBox(this);
      group.sizer.add(treeBox);

      treeBox.alternateRowColor = true;
      treeBox.headerVisible = false;
      treeBox.horizontalScrollBarVisible = true;
      treeBox.indentSize = 0;
      treeBox.multipleSelection = true;
      treeBox.numberOfColumns = 2;
      treeBox.setHeaderAlignment(0, TextAlign_Left | TextAlign_VertCenter);
      //console.writeln("lineSpacing: ", treeBox.font.lineSpacing);
      //console.writeln("borderWidth: ", treeBox.borderWidth);
      treeBox.setFixedHeight(
         this.displayPixelRatio * (rows + 1) * (treeBox.font.lineSpacing + 6) +
         treeBox.borderWidth
      );
      treeBox.showColumn(1, false);

      for (var i = 0; i < paths.length; ++i) {
         var node = new TreeBoxNode(treeBox);
         node.selectable = rows != 1;
         node.setText(0, fullPaths ? paths[i] : File.extractNameAndExtension(paths[i]));
      }
      treeBox.adjustColumnWidthToContents(0);

      return treeBox;
   };

   this.addPushButton = function(pane, text, toolTip, onClick) {
      var pushButton = new PushButton(this);
      pane.add(pushButton);

      pushButton.text = text;
      pushButton.toolTip = toolTip;
      pushButton.onClick = onClick;

      return pushButton;
   };

   this.addLabel = function(pane, text, toolTip) {
      var label = new Label(this);
      pane.add(label);

      label.setFixedWidth(this.labelWidth);
      label.text = text;
      label.toolTip = toolTip;
      label.textAlignment = TextAlign_Right | TextAlign_VertCenter;

      return label;
   };

   this.addEdit = function(pane, text, toolTip, onTextUpdated, onEditCompleted) {
      var edit = new Edit(this);
      pane.add(edit);

      edit.setFixedWidth(this.editWidth);
      edit.text = text;
      edit.toolTip = toolTip;
      edit.onTextUpdated = onTextUpdated;
      edit.onEditCompleted = onEditCompleted;

      return edit;
   };

   this.addUnit = function(pane, text) {
      var label = new Label(this);
      pane.add(label);

      label.setFixedWidth(this.unitWidth);
      label.text = text;
      label.textAlignment = TextAlign_Left | TextAlign_VertCenter;

      return label;
   };

   this.addComboBox = function(pane, items, currentItem, toolTip, onItemSelected) {
      var comboBox = new ComboBox(this);
      pane.add(comboBox);

      for (var i = 0; i != items.length; ++i) {
         comboBox.addItem(items[i]);
      }
      comboBox.currentItem = currentItem;
      comboBox.toolTip = toolTip;
      comboBox.onItemSelected = onItemSelected;
      return comboBox;
   };

   this.sizer = new VerticalSizer();
   this.sizer.margin = 6;
   this.sizer.spacing = 6;

   this.labelWidth = this.parent.font.width("Label 0:");
   this.editWidth = this.parent.font.width("00000000000");
   this.unitWidth = this.parent.font.width("unit");

   {
      this.group1Box = this.addGroupBox("Group1:");

      this.button1Pane = this.addPane(this.group1Box);

      this.observationWavelengthTypicalValuesComboBox = this.addComboBox(
         this.button1Pane,
         ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta", "Eta", "Theta"],
         0,
         "<p></p>",
         function(item) {}
      );

      this.button1Pane.addStretch();
   }

   {
      this.group1Box = this.addGroupBox("Group2:");
   }

   this.sizer.addStretch();
}
Tab2View.prototype = new Frame;

function MainView(model, controller) {
   this.__base__ = Dialog;
   this.__base__();

   this.addPane = function(group) {
      var buttonPane = new HorizontalSizer;
      buttonPane.spacing = 6;
      group.sizer.add(buttonPane);

      return buttonPane;
   };

   this.addToolButtonMousePress = function(pane, icon, toolTip, onMousePress) {
      var toolButton = new ToolButton(this);
      pane.add(toolButton);

      toolButton.icon = this.scaledResource(icon);
      toolButton.setScaledFixedSize(20, 20);
      toolButton.toolTip = toolTip;
      toolButton.onMousePress = onMousePress;

      return toolButton;
   };

   this.addToolButton = function(pane, icon, toolTip, onClick) {
      var toolButton = new ToolButton(this);
      pane.add(toolButton);

      toolButton.icon = this.scaledResource(icon);
      toolButton.setScaledFixedSize(20, 20);
      toolButton.toolTip = toolTip;
      toolButton.onClick = onClick;

      return toolButton;
   };

   this.addPushButton = function(pane, text, toolTip, onClick) {
      var pushButton = new PushButton(this);
      pane.add(pushButton);

      pushButton.text = text;
      pushButton.toolTip = toolTip;
      pushButton.onClick = onClick;

      return pushButton;
   };

   this.sizer = new VerticalSizer;
   this.sizer.margin = 6;
   this.sizer.spacing = 6;

   {
      this.tabBox = new TabBox(this);
      this.sizer.add(this.tabBox);

      this.tab1View = new Tab1View(this);
      this.tabBox.addPage(this.tab1View, "Tab1");

      this.tab2View = new Tab2View(this);
      this.tabBox.addPage(this.tab2View, "Tab2");
   }

   {
      this.buttonPane = this.addPane(this);

      this.newInstanceButton = this.addToolButtonMousePress(
         this.buttonPane,
         ":/process-interface/new-instance.png",
         "<p>Create a new instance.</p>",
         function() {
            this.hasFocus = true;
            this.pushed = false;
            this.dialog.newInstance();
         }
      );

      this.browseDocumentationButton = this.addToolButton(
         this.buttonPane,
         ":/process-interface/browse-documentation.png",
         "<p>Open a browser to view documentation.</p>",
         function() {
            if (!Dialog.browseScriptDocumentation(TITLE)) {
               (new MessageBox(
                  "<p>Documentation has not been installed.</p>",
                  TITLE,
                  StdIcon_Warning,
                  StdButton_Ok
               )).execute();
            }
         }
      );

      this.buttonPane.addStretch();

      this.dismissButton = this.addPushButton(
         this.buttonPane,
         "Dismiss",
         "<p>Dismiss the dialog.</p>",
         function() {
            this.dialog.ok();
         }
      );
      this.dismissButton.defaultButton = true;
   }

   this.windowTitle = TITLE;

   this.adjustToContents();
   this.setScaledMinWidth(300);
   //this.setScaledMinSize(300, 300);
   this.setFixedHeight( this.height + this.logicalPixelsToPhysical( 4 ) ); /* ### */
}
MainView.prototype = new Dialog;

function main() {
   console.hide();
   var mainView = new MainView();
   mainView.execute();
}

main();

where you'll find three modifications marked with /* ### */ comments. There are two modifications for GroupBox and One for the dialog.

For some reason that eludes me completely, GroupBox is not applying the application style sheet in this script (see the /rsc/qss/core-standard.qss style sheet file in your PI distribution). This is very strange and obviously a Qt bug, probably caused by the fact that these QGroupBox widgets are grandchildren of a QTabWidget. To fix this problem I have added this line to your addGroupBox functions:

      groupBox.styleSheet = "{}";

This simple assignment forces a complete recalculation of internal style properties and reapplies the application style sheet. Note that an empty string does not work because it is "optimized out" by Qt, hence the pair of curly brackets are necessary to represent an empty style.

The second change is this one:

   this.setFixedHeight( this.height + this.logicalPixelsToPhysical( 4 ) );

which replaces the usual call to this.setFixedHeight(). The four extra (logical) pixels are sufficient to "embed" the layout inaccuracies caused by the mix of TabBox and GroupBox controls.

On the Linux machine where I'm writing this, these changes fix all layout problems exposed by this script. Let me know if they work also on your Windows 7.

The solutions that I've found with this test script can be implemented in native code transparently. I still have to make tests on all platforms, but I am quite confident that they will work reliably. If this is the case, these improvements will be included in the next version.
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline mschuster

  • PTeam Member
  • PixInsight Jedi
  • *****
  • Posts: 1087
Re: PJSR: 1171 Dialog layout issues
« Reply #8 on: 2015 October 09 21:16:49 »
Hi Juan,

Thank you.

Preliminary feedback on Win7 for UI scaling 1: 4 logical pixels is not enough. 6 is necessary for these three scripts:

    this.setFixedHeight( this.height + this.logicalPixelsToPhysical( 6 ) );

I will try 6 in other scripts and at other UI scalings.

Thanks,
Mike

Offline mschuster

  • PTeam Member
  • PixInsight Jedi
  • *****
  • Posts: 1087
Re: PJSR: 1171 Dialog layout issues
« Reply #9 on: 2015 October 10 12:55:21 »
Hi Juan,

Per above, 6 logical pixels is not enough either. I found a situation where 10 is required (script WavefrontEstimator, 1185 Win7 low-res, UI scaling factor: 1, Low-resolution font: Open Sans, Core Style Sheet File Interface window styles font-size: 9pt).

Edit: 16 logical pixels required for 8pt Open Sans at UI scaling 1.5 and 2.0. I can't test larger UI scalings on my laptop.

Thanks,
Mike

PS: Juan, I am having a similar problem with horizontal layout. A GroupBox contains a multi-column TreeBox. Default layout does not show the entire TreeBox. Dialog.adjustToContents() sets width to a value not large enough. I can call Dialog.setMinWidth(), but I have no good way to determine a good value that works across arbitrary user font changes, user qss changes, and UI scaling factor changes.

Edit: Regarding width, I decided to use a similar workaround:

    this.setFixedWidth(this.width + this.logicalPixelsToPhysical(<constant>));

where <constant> is tbd.
« Last Edit: 2015 October 11 09:38:11 by mschuster »