Repository: pyqtgraph/pyqtgraph

Issue:
TickSliderItem allows removal when allowAdd == False
A `TickSliderItem` has a `allowAdd` optional initialization argument. When it is set to False, it is not possible to add additional ticks. However, the current behavior allows the user to remove the ticks, but does not allow the user to add them back. The correct behavior probably should be to disable removal when `allowAdd` is False.
TickSliderItem allows removal when allowAdd == False
A `TickSliderItem` has a `allowAdd` optional initialization argument. When it is set to False, it is not possible to add additional ticks. However, the current behavior allows the user to remove the ticks, but does not allow the user to add them back. The correct behavior probably should be to disable removal when `allowAdd` is False.


Discussion:
It is possible to prohibit removing a tick using `tick.removeAllowed = False`. Therefore prohibiting the removal of any tick would be performed setting `removeAllowed` to `False`, e.g. using `TickSliderItem.listTicks`.

It turns out that this mechanism is broken, so I fix it in PR #1440. 

Should `allowAdd` also default `removeAllowed` to `False` for new ticks? I have no strong opinion on this design decision, and would therefore pledge not to change it (every change is a potential cause for error for other users).
I would suggest to add a new `allowRemove` property that acts as the default of `tick.removeAllowed`, and will add a PR to open discussion on that.

In the meantime, here is an example code of what works with PR #1440:

```python3
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
import numpy as np

app = pg.mkQApp()

class CustomWidget(pg.GraphicsView):
    def __init__(self, parent=None, *args, **kargs):
        pg.GraphicsView.__init__(self, parent, useOpenGL=False, background=None)
        self.item = pg.TickSliderItem(*args, **kargs)

        for pos in (0, 0.5, 1):
            tick = self.item.addTick(pos)
            tick.removeAllowed = False # Possibility 1

        # Possibility 2
        for tick, value in self.item.listTicks():
            tick.removeAllowed = False

        self.setCentralItem(self.item)
        self.setFixedHeight(31)

w = CustomWidget()
w.show()

app.exec_()
```
Update: I noticed that `removeAllowed` is not a regular property of `Tick`, but monkeypatched by `GradientEditorItem`. Therefore this was no bug. I updated my PR to #1441, making it a regular property of `Tick` and enabling its use with `TickSliderItem`.
It is possible to prohibit removing a tick using `tick.removeAllowed = False`. Therefore prohibiting the removal of any tick would be performed setting `removeAllowed` to `False`, e.g. using `TickSliderItem.listTicks`.

It turns out that this mechanism is broken, so I fix it in PR #1440. 

Should `allowAdd` also default `removeAllowed` to `False` for new ticks? I have no strong opinion on this design decision, and would therefore pledge not to change it (every change is a potential cause for error for other users).
I would suggest to add a new `allowRemove` property that acts as the default of `tick.removeAllowed`, and will add a PR to open discussion on that.

In the meantime, here is an example code of what works with PR #1440:

```python3
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
import numpy as np

app = pg.mkQApp()

class CustomWidget(pg.GraphicsView):
    def __init__(self, parent=None, *args, **kargs):
        pg.GraphicsView.__init__(self, parent, useOpenGL=False, background=None)
        self.item = pg.TickSliderItem(*args, **kargs)

        for pos in (0, 0.5, 1):
            tick = self.item.addTick(pos)
            tick.removeAllowed = False # Possibility 1

        # Possibility 2
        for tick, value in self.item.listTicks():
            tick.removeAllowed = False

        self.setCentralItem(self.item)
        self.setFixedHeight(31)

w = CustomWidget()
w.show()

app.exec_()
```
Update: I noticed that `removeAllowed` is not a regular property of `Tick`, but monkeypatched by `GradientEditorItem`. Therefore this was no bug. I updated my PR to #1441, making it a regular property of `Tick` and enabling its use with `TickSliderItem`.

Resolving patch:
diff --git a/examples/GradientWidget.py b/examples/GradientWidget.py
--- a/examples/GradientWidget.py
+++ b/examples/GradientWidget.py
@@ -27,15 +27,18 @@
 w1 = pg.GradientWidget(orientation='top')
 w2 = pg.GradientWidget(orientation='right', allowAdd=False)
 #w2.setTickColor(1, QtGui.QColor(255,255,255))
-w3 = pg.GradientWidget(orientation='bottom')
+w3 = pg.GradientWidget(orientation='bottom', allowAdd=False, allowRemove=False)
 w4 = pg.GradientWidget(orientation='left')
 w4.loadPreset('spectrum')
 label = QtGui.QLabel("""
 - Click a triangle to change its color
 - Drag triangles to move
+- Right-click a gradient to load triangle presets
 - Click in an empty area to add a new color
-    (adding is disabled for the right-side widget)
+    (adding is disabled for the bottom-side and right-side widgets)
 - Right click a triangle to remove
+    (only possible if more than two triangles are visible)
+    (removing is disabled for the bottom-side widget)
 """)
 
 l.addWidget(w1, 0, 1)
diff --git a/examples/customPlot.py b/examples/customPlot.py
--- a/examples/customPlot.py
+++ b/examples/customPlot.py
@@ -30,15 +30,60 @@ def mouseDragEvent(self, ev, axis=None):
         else:
             pg.ViewBox.mouseDragEvent(self, ev, axis=axis)
 
+class CustomTickSliderItem(pg.TickSliderItem):
+    def __init__(self, *args, **kwds):
+        pg.TickSliderItem.__init__(self, *args, **kwds)
+        
+        self.all_ticks = []
+        self.visible_ticks = {}
+        self._range = [0,1]
+    
+    def setTicks(self, ticks):
+        for tick in tickViewer.listTicks():
+            self.removeTick(tick)
+            self.visible_ticks = {}
+        
+        self.all_ticks = ticks
+        
+        self.updateRange(None, self._range)
+    
+    def updateRange(self, vb, viewRange):
+        self._range = viewRange
+        
+        for pos in self.all_ticks:
+            visible = pos >= viewRange[0] and pos <= viewRange[1]
+            
+            if visible:
+                if pos not in self.visible_ticks:
+                    self.visible_ticks[pos] = self.addTick(pos, movable=False, color="333333")
+                
+                tick = self.visible_ticks[pos]
+                
+                tickValue = (pos - viewRange[0]) / (viewRange[1] - viewRange[0])
+                self.setTickValue(tick, tickValue)
+            
+            elif pos in self.visible_ticks:
+                self.removeTick(self.visible_ticks[pos])
+                del self.visible_ticks[pos]
+
 
 app = pg.mkQApp()
 
 axis = pg.DateAxisItem(orientation='bottom')
 vb = CustomViewBox()
 
-pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with DateAxisItem and custom ViewBox<br>Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom")
+pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with DateAxisItem, custom ViewBox and markers on x axis<br>Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom")
+
 dates = np.arange(8) * (3600*24*356)
 pw.plot(x=dates, y=[1,6,2,4,3,5,6,8], symbol='o')
+
+# Using allowAdd and allowRemove to limit user interaction
+tickViewer = CustomTickSliderItem(allowAdd=False, allowRemove=False)
+vb.sigXRangeChanged.connect(tickViewer.updateRange)
+pw.plotItem.layout.addItem(tickViewer, 4, 1)
+
+tickViewer.setTicks( [dates[0], dates[2], dates[-1]] )
+
 pw.show()
 pw.setWindowTitle('pyqtgraph example: customPlot')
 
diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py
--- a/pyqtgraph/graphicsItems/GradientEditorItem.py
+++ b/pyqtgraph/graphicsItems/GradientEditorItem.py
@@ -48,13 +48,14 @@ class TickSliderItem(GraphicsWidget):
     sigTicksChanged = QtCore.Signal(object)
     sigTicksChangeFinished = QtCore.Signal(object)
     
-    def __init__(self, orientation='bottom', allowAdd=True, **kargs):
+    def __init__(self, orientation='bottom', allowAdd=True, allowRemove=True, **kargs):
         """
         ==============  =================================================================================
         **Arguments:**
         orientation     Set the orientation of the gradient. Options are: 'left', 'right'
                         'top', and 'bottom'.
-        allowAdd        Specifies whether ticks can be added to the item by the user.
+        allowAdd        Specifies whether the user can add ticks.
+        allowRemove     Specifies whether the user can remove new ticks.
         tickPen         Default is white. Specifies the color of the outline of the ticks.
                         Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
         ==============  =================================================================================
@@ -67,6 +68,7 @@ def __init__(self, orientation='bottom', allowAdd=True, **kargs):
         self.ticks = {}
         self.maxDim = 20
         self.allowAdd = allowAdd
+        self.allowRemove = allowRemove
         if 'tickPen' in kargs:
             self.tickPen = fn.mkPen(kargs['tickPen'])
         else:
@@ -166,7 +168,7 @@ def addTick(self, x, color=None, movable=True, finish=True):
         
         if color is None:
             color = QtGui.QColor(255,255,255)
-        tick = Tick([x*self.length, 0], color, movable, self.tickSize, pen=self.tickPen)
+        tick = Tick([x*self.length, 0], color, movable, self.tickSize, pen=self.tickPen, removeAllowed=self.allowRemove)
         self.ticks[tick] = x
         tick.setParentItem(self)
         
@@ -863,7 +865,7 @@ class Tick(QtGui.QGraphicsWidget):  ## NOTE: Making this a subclass of GraphicsO
     sigMoved = QtCore.Signal(object)
     sigClicked = QtCore.Signal(object, object)
     
-    def __init__(self, pos, color, movable=True, scale=10, pen='w'):
+    def __init__(self, pos, color, movable=True, scale=10, pen='w', removeAllowed=True):
         self.movable = movable
         self.moving = False
         self.scale = scale
@@ -871,7 +873,7 @@ def __init__(self, pos, color, movable=True, scale=10, pen='w'):
         self.pen = fn.mkPen(pen)
         self.hoverPen = fn.mkPen(255,255,0)
         self.currentPen = self.pen
-        self.removeAllowed = True
+        self.removeAllowed = removeAllowed
         self.pg = QtGui.QPainterPath(QtCore.QPointF(0,0))
         self.pg.lineTo(QtCore.QPointF(-scale/3**0.5, scale))
         self.pg.lineTo(QtCore.QPointF(scale/3**0.5, scale))


Test patch: