Repository: biolab/orange3

Issue:
OWMergeData: + and x symbols are small and hard to click
Not sure if this happens only on Windows or elsewhere too.

It should be implemented like the buttons are in OWCreateClass.


Discussion:
I'd prefer it to be not like Create Class. This is one of the non-nicer widgets I've seen lately. :)

<img width="356" alt="Screenshot 2021-03-11 at 09 25 18" src="https://user-images.githubusercontent.com/2387315/110757497-beca0200-824b-11eb-8bcc-c66e51c27be6.png">

Meanwhile, Merge Data looks OK and I've no problems clicking the symbol.

<img width="403" alt="Screenshot 2021-03-11 at 09 27 41" src="https://user-images.githubusercontent.com/2387315/110757829-1c5e4e80-824c-11eb-8517-214c096e0b50.png">

That's so weird.
This is what Create Class looks like for me:

<img width="468" alt="Screenshot 2021-03-11 at 13 38 54" src="https://user-images.githubusercontent.com/24586651/110795779-2180c500-826f-11eb-807f-91af9ac8b5cb.png">
Janez was on PyQt5.9. On our currently supported versions, Create Class looks as it does in my screenshot.

Resolving patch:
diff --git a/Orange/widgets/data/owmergedata.py b/Orange/widgets/data/owmergedata.py
--- a/Orange/widgets/data/owmergedata.py
+++ b/Orange/widgets/data/owmergedata.py
@@ -5,8 +5,9 @@
 
 from AnyQt.QtCore import Qt, QModelIndex, pyqtSignal as Signal
 from AnyQt.QtWidgets import (
-    QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout
+    QWidget, QLabel, QVBoxLayout, QHBoxLayout, QSizePolicy
 )
+
 from orangewidget.utils.combobox import ComboBoxSearch
 
 import Orange
@@ -29,8 +30,7 @@ class ConditionBox(QWidget):
 
     RowItems = namedtuple(
         "RowItems",
-        ("pre_label", "left_combo", "in_label", "right_combo",
-         "remove_button", "add_button"))
+        ("pre_label", "left_combo", "in_label", "right_combo", "remove_button"))
 
     def __init__(self, parent, model_left, model_right, pre_label, in_label):
         super().__init__(parent)
@@ -43,6 +43,12 @@ def __init__(self, parent, model_left, model_right, pre_label, in_label):
         self.layout().setSpacing(0)
         self.setMouseTracking(True)
 
+    def get_button(self, label, callback):
+        return gui.button(
+            None, self, label, callback=callback,
+            addToLayout=False, autoDefault=False, width=34,
+            sizePolicy=(QSizePolicy.Maximum, QSizePolicy.Maximum))
+
     def add_row(self):
         def sync_combos():
             combo = self.sender()
@@ -78,31 +84,29 @@ def get_combo(model):
             combo.activated.connect(sync_combos)
             return combo
 
-        def get_button(label, callback):
-            button = QPushButton(label, self)
-            button.setFlat(True)
-            button.setFixedWidth(12)
-            button.clicked.connect(callback)
-            return button
-
         row = self.layout().count()
         row_items = self.RowItems(
             QLabel("and" if row else self.pre_label),
             get_combo(self.model_left),
             QLabel(self.in_label),
             get_combo(self.model_right),
-            get_button("×", self.on_remove_row),
-            get_button("+", self.on_add_row)
+            self.get_button("×", self.on_remove_row)
         )
         layout = QHBoxLayout()
         layout.setSpacing(10)
-        self.layout().addLayout(layout)
+        self.layout().insertLayout(self.layout().count() - 1, layout)
         layout.addStretch(10)
         for item in row_items:
             layout.addWidget(item)
         self.rows.append(row_items)
         self._reset_buttons()
 
+    def add_plus_row(self):
+        layout = QHBoxLayout()
+        self.layout().addLayout(layout)
+        layout.addStretch(1)
+        layout.addWidget(self.get_button("+", self.on_add_row))
+
     def remove_row(self, row):
         self.layout().takeAt(self.rows.index(row))
         self.rows.remove(row)
@@ -125,16 +129,8 @@ def on_remove_row(self):
         self.emit_list()
 
     def _reset_buttons(self):
-        def endis(button, enable, text):
-            button.setEnabled(enable)
-            button.setText(text if enable else "")
-
         self.rows[0].pre_label.setText(self.pre_label)
-        single_row = len(self.rows) == 1
-        endis(self.rows[0].remove_button, not single_row, "×")
-        endis(self.rows[-1].add_button, True, "+")
-        if not single_row:
-            endis(self.rows[-2].add_button, False, "")
+        self.rows[0].remove_button.setDisabled(len(self.rows) == 1)
 
     def current_state(self):
         def get_var(model, combo):
@@ -333,6 +329,7 @@ def __init__(self):
         self.attr_boxes = ConditionBox(
             self, self.model, self.extra_model, "", "matches")
         self.attr_boxes.add_row()
+        self.attr_boxes.add_plus_row()
         box = gui.vBox(self.controlArea, box="Row matching")
         box.layout().addWidget(self.attr_boxes)
 


Test patch:
diff --git a/Orange/widgets/data/tests/test_owmergedata.py b/Orange/widgets/data/tests/test_owmergedata.py
--- a/Orange/widgets/data/tests/test_owmergedata.py
+++ b/Orange/widgets/data/tests/test_owmergedata.py
@@ -214,9 +214,11 @@ def test_match_attr_name(self):
     def test_add_row_button(self):
         boxes = self.widget.attr_boxes
         boxes.set_state([(INSTANCEID, INSTANCEID), (INSTANCEID, INSTANCEID)])
-        boxes.rows[-1].add_button.clicked.emit()
+        layout = boxes.layout()
+        add_button = layout.itemAt(layout.count() - 1).itemAt(1).widget()
+        add_button.clicked.emit()
         self.assertEqual(len(boxes.rows), 3)
-        self.assertEqual(boxes.layout().count(), 3)
+        self.assertEqual(boxes.layout().count(), 4)
 
     def test_remove_row(self):
         widget = self.widget
@@ -228,38 +230,26 @@ def test_remove_row(self):
 
         boxes.set_state(
             [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])
-        for i, row in enumerate(boxes.rows):
+        for row in boxes.rows:
             self.assertTrue(row.remove_button.isEnabled())
-            self.assertEqual(row.remove_button.text(), "×")
-            self.assertEqual(row.add_button.isEnabled(), i == 2)
-            self.assertEqual(row.add_button.text(), ["", "+"][i == 2])
 
         boxes.rows[1].remove_button.clicked.emit()
         self.assertEqual(boxes.current_state(), [(INDEX, INDEX), (var0, var1)])
-        for i, row in enumerate(boxes.rows):
+        for row in boxes.rows:
             self.assertTrue(row.remove_button.isEnabled())
-            self.assertEqual(row.remove_button.text(), "×")
-            self.assertEqual(row.add_button.isEnabled(), i == 1)
-            self.assertEqual(row.add_button.text(), ["", "+"][i])
 
         boxes.rows[1].remove_button.clicked.emit()
         self.assertEqual(boxes.current_state(), [(INDEX, INDEX)])
         row = boxes.rows[0]
         self.assertFalse(row.remove_button.isEnabled())
-        self.assertEqual(row.remove_button.text(), "")
-        self.assertTrue(row.add_button.isEnabled())
-        self.assertEqual(row.add_button.text(), "+")
 
         boxes.set_state(
             [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])
         boxes.rows[2].remove_button.clicked.emit()
         self.assertEqual(
             boxes.current_state(), [(INDEX, INDEX), (INSTANCEID, INSTANCEID)])
-        for i, row in enumerate(boxes.rows):
+        for row in boxes.rows:
             self.assertTrue(row.remove_button.isEnabled())
-            self.assertEqual(row.remove_button.text(), "×")
-            self.assertEqual(row.add_button.isEnabled(), i == 1)
-            self.assertEqual(row.add_button.text(), ["", "+"][i == 1])
 
     def test_dont_remove_single_row(self):
         widget = self.widget