Commit 3098f60b authored by Adrien's avatar Adrien Committed by GitHub

fix(organization): User page tab folders are unusable on MacOs Big Sur (#2514)

2 new classes in studio.ui : nativeTabFolderWidget and
NativeTabFolderItem

Depending on the OS, thoses create TabFolder & TabItem (mac and linux)
or CTabFolder (windows)
& CTabItem

-> TabFolder and TabItem have a nice rendering on mac and linux, but a
bad one on windows. It is the opposit for CTabItem and CTabFolder
-> Those two classes ensure that the best rendering is always use, with
the same behavior

Use CTab folders / items on macos since TabFolder are natively broken
since the macos update. This way, we ensure that the behavior is the
same no matter the macos version. The rendering is still nice thanks to
the custom css rules on CTabFolders.

[STUDIO-3703](https://bonitasoft.atlassian.net/browse/STUDIO-3703)
parent 192df697
......@@ -21,6 +21,7 @@ import org.bonitasoft.studio.actors.model.organization.Membership;
import org.bonitasoft.studio.actors.model.organization.Organization;
import org.bonitasoft.studio.actors.model.organization.Role;
import org.bonitasoft.studio.actors.model.organization.User;
import org.bonitasoft.studio.ui.widget.NativeTabFolderWidget;
import org.eclipse.emf.databinding.EMFDataBindingContext;
import org.eclipse.jface.databinding.wizard.WizardPageSupport;
import org.eclipse.jface.layout.GridDataFactory;
......@@ -68,7 +69,7 @@ public abstract class AbstractOrganizationWizardPage extends WizardPage implemen
protected List<Role> roleList;
protected EMFDataBindingContext context;
protected WizardPageSupport pageSupport;
protected TabFolder tabFolder;
protected NativeTabFolderWidget tabFolder;
protected Composite mainComposite;
protected AbstractOrganizationWizardPage(final String pageName) {
......
......@@ -24,8 +24,6 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bonitasoft.studio.actors.ActorsPlugin;
import org.bonitasoft.studio.actors.i18n.Messages;
......@@ -45,6 +43,8 @@ import org.bonitasoft.studio.common.NamingUtils;
import org.bonitasoft.studio.common.jface.TableColumnSorter;
import org.bonitasoft.studio.common.jface.databinding.validator.EmptyInputValidator;
import org.bonitasoft.studio.pics.Pics;
import org.bonitasoft.studio.ui.widget.NativeTabFolderWidget;
import org.bonitasoft.studio.ui.widget.NativeTabItemWidget;
import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.conversion.Converter;
......@@ -53,6 +53,7 @@ import org.eclipse.core.databinding.observable.list.IListChangeListener;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.databinding.observable.list.ListChangeEvent;
import org.eclipse.core.databinding.observable.list.WritableList;
import org.eclipse.core.databinding.observable.value.ComputedValue;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.IValueChangeListener;
import org.eclipse.core.databinding.observable.value.ValueChangeEvent;
......@@ -72,9 +73,11 @@ import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport;
import org.eclipse.jface.databinding.fieldassist.ControlDecorationUpdater;
import org.eclipse.jface.databinding.swt.SWTObservables;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.jface.databinding.viewers.IViewerObservableValue;
import org.eclipse.jface.databinding.viewers.ObservableListContentProvider;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.databinding.viewers.typed.ViewerProperties;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.fieldassist.ControlDecoration;
import org.eclipse.jface.layout.GridDataFactory;
......@@ -107,13 +110,10 @@ import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.PlatformUI;
/**
* @author Romain Bioteau
......@@ -128,20 +128,20 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
CustomUserInfoDefinitions infoDefinitions;
private TabItem generalTab;
private TabItem personalTab;
private TabItem professionnalTab;
private TabFolder tab;
private NativeTabItemWidget generalTab;
private NativeTabItemWidget personalTab;
private NativeTabItemWidget professionnalTab;
private NativeTabFolderWidget tab;
private final List<Membership> userMemberShips = new ArrayList<>();
private TabItem memberShipTab;
private TabItem customTab;
private TabItem infoTab;
private NativeTabItemWidget memberShipTab;
private NativeTabItemWidget customTab;
private NativeTabItemWidget infoTab;
TableViewer customUserInfoTable;
private IObservableList customUserInfoObservableList;
private CustomUserInformationDefinitionNameEditingSupport customUserInformationDefinitionNameEditingSupport;
private IViewerObservableValue userSingleSelectionObservable;
private TabItem userTab;
private NativeTabItemWidget userTab;
private Composite labelComposite;
private ComboViewer managerNameComboViewer;
private SelectionAdapter selectionAdapter;
......@@ -177,16 +177,12 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
}
}
final TabItem item = tab.getSelection()[0];
final NativeTabItemWidget item = tab.getSelection();
if (item.equals(memberShipTab)) {
for (final Control c : tab.getChildren()) {
c.dispose();
}
disposeTabItemControl(memberShipTab);
refreshMembershipTab();
} else if (item.equals(customTab)) {
for (final Control c : tab.getChildren()) {
c.dispose();
}
disposeTabItemControl(customTab);
refreshCustomTab();
}
......@@ -295,94 +291,107 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
protected void configureInfoGroup(final Group group) {
group.setText(Messages.details);
final Composite detailsComposite = new Composite(group, SWT.NONE);
detailsComposite.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).span(2, 1).create());
detailsComposite.setLayout(
GridLayoutFactory.fillDefaults().numColumns(2).spacing(0, 2).margins(15, 5).equalWidth(false).create());
Composite detailsComposite = new Composite(group, SWT.NONE);
detailsComposite.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).span(2, 1).create());
detailsComposite.setLayout(GridLayoutFactory.fillDefaults().create());
IViewerObservableValue<Object> selection = ViewerProperties.singleSelection().observe(getViewer());
context.bindValue(WidgetProperties.visible().observe(detailsComposite), new ComputedValue<Boolean>() {
createUserNameField(detailsComposite);
@Override
protected Boolean calculate() {
return selection.getValue() != null;
}
});
createPasswordField(detailsComposite);
Composite generalDetailsComposite = new Composite(detailsComposite, SWT.NONE);
generalDetailsComposite.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create());
generalDetailsComposite.setLayout(GridLayoutFactory.fillDefaults().spacing(0, 2).create());
createManagerCombo(detailsComposite);
createUserNameField(generalDetailsComposite);
createPasswordField(generalDetailsComposite);
createManagerCombo(generalDetailsComposite);
tab = new TabFolder(group, SWT.NONE);
tab.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).span(2, 1).create());
tab.setLayout(GridLayoutFactory.fillDefaults().numColumns(1).create());
tab = new NativeTabFolderWidget.Builder().createIn(detailsComposite);
tab.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create());
tab.setLayout(GridLayoutFactory.fillDefaults().create());
selectionAdapter = new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent e) {
Control control = null;
final TabItem item = (TabItem) e.item;
for (final Control c : tab.getChildren()) {
c.dispose();
}
Object item = e.item;
if (item.equals(generalTab)) {
if (item.equals(generalTab.getItem())) {
disposeTabItemControl(generalTab);
final ScrolledComposite sc = createScrolledComposite();
control = createGeneralControl(sc);
updatedScrolMinSize(control, sc);
sc.setContent(control);
generalTab.setControl(sc);
} else if (item.equals(personalTab)) {
} else if (item.equals(personalTab.getItem())) {
disposeTabItemControl(personalTab);
final ScrolledComposite sc = createScrolledComposite();
control = createContactInfoControl(sc, OrganizationPackage.Literals.USER__PERSONAL_DATA);
updatedScrolMinSize(control, sc);
sc.setContent(control);
personalTab.setControl(sc);
} else if (item.equals(professionnalTab)) {
} else if (item.equals(professionnalTab.getItem())) {
disposeTabItemControl(professionnalTab);
final ScrolledComposite sc = createScrolledComposite();
control = createContactInfoControl(sc, OrganizationPackage.Literals.USER__PROFESSIONAL_DATA);
updatedScrolMinSize(control, sc);
sc.setContent(control);
professionnalTab.setControl(sc);
}
else if (item.equals(memberShipTab)) {
else if (item.equals(memberShipTab.getItem())) {
disposeTabItemControl(memberShipTab);
final ScrolledComposite sc = createScrolledComposite();
control = createMembershipControl(sc);
updatedScrolMinSize(control, sc);
sc.setContent(control);
memberShipTab.setControl(sc);
}
else if (item.equals(customTab)) {
else if (item.equals(customTab.getItem())) {
disposeTabItemControl(customTab);
final ScrolledComposite sc = createScrolledComposite();
control = createCustomControl(sc);
updatedScrolMinSize(control, sc);
sc.setContent(control);
customTab.setControl(sc);
}
getInfoGroup().layout(true, true);
}
};
tab.addSelectionListener(selectionAdapter);
generalTab = new TabItem(tab, SWT.NONE);
generalTab.setText(Messages.general);
generalTab = new NativeTabItemWidget.Builder()
.withText(Messages.general)
.createIn(tab);
memberShipTab = new TabItem(tab, SWT.SCROLL_LINE);
memberShipTab.setText(Messages.membership + " *");
memberShipTab = new NativeTabItemWidget.Builder()
.withText(Messages.membership + " *")
.withStyle(SWT.SCROLL_LINE)
.createIn(tab);
personalTab = new TabItem(tab, SWT.NONE);
personalTab.setText(Messages.personalData);
personalTab = new NativeTabItemWidget.Builder()
.withText(Messages.personalData)
.createIn(tab);
professionnalTab = new TabItem(tab, SWT.NONE);
professionnalTab.setText(Messages.professionalData);
professionnalTab = new NativeTabItemWidget.Builder()
.withText(Messages.professionalData)
.createIn(tab);
customTab = new TabItem(tab, SWT.SCROLL_LINE);
customTab.setText(Messages.other);
customTab = new NativeTabItemWidget.Builder()
.withText(Messages.other)
.withStyle(SWT.SCROLL_LINE)
.createIn(tab);
tab.setSelection(generalTab);
......@@ -460,23 +469,23 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
final Text passwordText = new Text(passwordComposite, SWT.BORDER | SWT.PASSWORD);
passwordText.setLayoutData(
GridDataFactory.swtDefaults()
.align(SWT.FILL, SWT.CENTER)
.grab(true, false)
.indent(5, 0)
.span("macosx".equals(Platform.getOS()) ? 2 : 1, 1)
.create());
.align(SWT.FILL, SWT.CENTER)
.grab(true, false)
.indent(5, 0)
.span("macosx".equals(Platform.getOS()) ? 2 : 1, 1)
.create());
char echoChar = passwordText.getEchoChar();
if(!"macosx".equals(Platform.getOS())) {
if (!"macosx".equals(Platform.getOS())) {
final Button viewPaswsordButton = new Button(passwordComposite, SWT.TOGGLE);
viewPaswsordButton.setLayoutData(GridDataFactory.swtDefaults().create());
viewPaswsordButton.setImage(Pics.getImage("view.png", ActorsPlugin.getDefault()));
viewPaswsordButton.setToolTipText(Messages.showPassword);
viewPaswsordButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
passwordText.setEchoChar(viewPaswsordButton.getSelection() ? CLEAR_CHAR : echoChar);
passwordText.setEchoChar(viewPaswsordButton.getSelection() ? CLEAR_CHAR : echoChar);
}
});
}
......@@ -650,7 +659,7 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
* @return
*/
private ScrolledComposite createScrolledComposite() {
final ScrolledComposite sc = new ScrolledComposite(tab, SWT.V_SCROLL);
final ScrolledComposite sc = new ScrolledComposite(tab.getTabFolder(), SWT.V_SCROLL);
sc.setLayout(GridLayoutFactory.fillDefaults().numColumns(1).margins(5, 5).create());
sc.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, SWT.DEFAULT).create());
sc.setExpandHorizontal(true);
......@@ -799,7 +808,7 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
m.setUserName(u.getUserName());
membershipList.add(m);
final Event ev = new Event();
ev.item = tab.getSelection()[0];
ev.item = tab.getSelection().getItem();
tab.notifyListeners(SWT.Selection, ev);
}
......@@ -808,7 +817,7 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
Messages.deleteMembershipMsg)) {
membershipList.remove(membership);
final Event ev = new Event();
ev.item = tab.getSelection()[0];
ev.item = tab.getSelection().getItem();
tab.notifyListeners(SWT.Selection, ev);
}
}
......@@ -1290,13 +1299,15 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
@Override
public void createControl(final Composite parent) {
tabFolder = new TabFolder(parent, SWT.TOP);
tabFolder = new NativeTabFolderWidget.Builder()
.createIn(parent);
//new TabFolder(parent, SWT.TOP);
tabFolder.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create());
tabFolder.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent e) {
if (e.item.equals(userTab)) {
if (e.item.equals(userTab.getItem())) {
refreshCustomTab();
refreshMembershipTab();
}
......@@ -1304,16 +1315,22 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
});
super.createControl(tabFolder);
super.setControl(tabFolder);
Composite userTabComposite = new Composite(tabFolder.getTabFolder(), SWT.NONE);
userTabComposite.setLayout(GridLayoutFactory.fillDefaults().create());
userTabComposite.setLayoutData(GridDataFactory.fillDefaults().create());
userTab = new NativeTabItemWidget.Builder()
.withText(Messages.listOfUsersTabTitle)
.withControl(userTabComposite)
.createIn(tabFolder);
userTab = new TabItem(tabFolder, SWT.NONE);
userTab.setText(Messages.listOfUsersTabTitle);
userTab.setControl(mainComposite);
super.createControl(userTabComposite);
super.setControl(userTabComposite);
infoTab = new TabItem(tabFolder, SWT.NONE);
infoTab.setText(Messages.customUserInformationTabTitle);
infoTab.setControl(addInformationComposite());
infoTab = new NativeTabItemWidget.Builder()
.withText(Messages.customUserInformationTabTitle)
.withControl(addInformationComposite())
.createIn(tabFolder);
tabFolder.setSelection(userTab);
}
......@@ -1346,7 +1363,7 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
}
private Composite addInformationComposite() {
final Composite infoCompo = new Composite(tabFolder, SWT.NONE);
final Composite infoCompo = new Composite(tabFolder.getTabFolder(), SWT.NONE);
infoCompo.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create());
infoCompo.setLayout(GridLayoutFactory.fillDefaults().numColumns(1).margins(10, 10).create());
......@@ -1701,4 +1718,11 @@ public class UsersWizardPage extends AbstractOrganizationWizardPage {
private boolean isNotUserSelected() {
return userSingleSelectionObservable == null || userSingleSelectionObservable.getValue() == null;
}
private void disposeTabItemControl(NativeTabItemWidget item) {
Control c = item.getControl();
if (c != null && !c.isDisposed()) {
c.dispose();
}
}
}
/**
* Copyright (C) 2020 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.studio.ui.widget;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.eclipse.core.runtime.Platform;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
/**
* On linux and osx, this widget creates a @TabFolder control.
* On windows, this widget creates a @CTabFolder control.
* -> The TabFolder visual rendering is really great on linux and macos,
* but pretty bad on windows (especially when css themes are used).
* This widget is used by @NativeTabItemWidget widgets, which are @TabItem or @CTabItem controls, dependening on the os.
*/
public class NativeTabFolderWidget {
private Composite parent;
private Composite tabFolder;
private List<NativeTabItemWidget> children = new ArrayList<>();
public static class Builder {
public NativeTabFolderWidget createIn(Composite parent) {
NativeTabFolderWidget widget = new NativeTabFolderWidget(parent);
widget.init();
return widget;
}
}
private NativeTabFolderWidget(Composite parent) {
this.parent = parent;
}
private void init() {
tabFolder = createNativeTabFolder();
}
private Composite createNativeTabFolder() {
if (useCTabFolder()) {
return new CTabFolder(parent, SWT.TOP);
}
return new TabFolder(parent, SWT.TOP);
}
private boolean useCTabFolder() {
return Objects.equals(Platform.OS_MACOSX, Platform.getOS()) || Objects.equals(Platform.OS_WIN32, Platform.getOS());
}
public Composite getTabFolder() {
return tabFolder;
}
public void addSelectionListener(SelectionListener listener) {
if (useCTabFolder()) {
((CTabFolder) tabFolder).addSelectionListener(listener);
} else {
((TabFolder) tabFolder).addSelectionListener(listener);
}
}
public void setSelection(NativeTabItemWidget tabItem) {
if (useCTabFolder()) {
((CTabFolder) tabFolder).setSelection((CTabItem) tabItem.getItem());
Event event = new Event();
event.item = tabItem.getItem();
((CTabFolder) tabFolder).notifyListeners(SWT.Selection, event);
} else {
((TabFolder) tabFolder).setSelection((TabItem) tabItem.getItem());
}
}
void addChild(NativeTabItemWidget tabItem) {
children.add(tabItem);
}
public NativeTabItemWidget getSelection() {
Object selection = useCTabFolder()
? ((CTabFolder) tabFolder).getSelection()
: ((TabFolder) tabFolder).getSelection()[0];
return children.stream()
.filter(child -> Objects.equals(child.getItem(), selection))
.findFirst().orElse(null);
}
public int getSelectionIndex() {
return useCTabFolder()
? ((CTabFolder) tabFolder).getSelectionIndex()
: ((TabFolder) tabFolder).getSelectionIndex();
}
public NativeTabItemWidget getItem(int index) {
Item item = useCTabFolder()
? ((CTabFolder) tabFolder).getItem(index)
: ((TabFolder) tabFolder).getItem(index);
return children.stream().filter(child -> Objects.equals(item, child.getItem())).findFirst().orElse(null);
}
public List<NativeTabItemWidget> getItems() {
return children;
}
public void notifyListeners(int eventType, Event event) {
if (useCTabFolder()) {
((CTabFolder) tabFolder).notifyListeners(eventType, event);
} else {
((TabFolder) tabFolder).notifyListeners(eventType, event);
}
}
public void setLayoutData(Object layoutData) {
tabFolder.setLayoutData(layoutData);
}
public void setLayout(Layout layout) {
tabFolder.setLayout(layout);
}
}
/**
* Copyright (C) 2020 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.studio.ui.widget;
import java.util.Objects;
import org.eclipse.core.runtime.Platform;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;