From 148a35d430f192841a6271b504a7f9794e666b54 Mon Sep 17 00:00:00 2001 From: Nils Nemitz Date: Mon, 8 Mar 2021 02:14:37 +0900 Subject: [PATCH] mostly functional example --- examples/PaletteApplicationExample.py | 153 ++++++++++++ examples/utils.py | 2 +- pyqtgraph/__init__.py | 8 +- pyqtgraph/functions.py | 219 +++++++++++------- pyqtgraph/graphicsItems/AxisItem.py | 4 +- pyqtgraph/graphicsItems/GraphicsObject.py | 3 +- pyqtgraph/graphicsItems/GraphicsWidget.py | 11 +- pyqtgraph/graphicsItems/LabelItem.py | 51 ++-- pyqtgraph/graphicsItems/ScatterPlotItem.py | 20 +- .../tests/test_ScatterPlotItem.py | 3 +- pyqtgraph/namedBrush.py | 80 +++++++ pyqtgraph/namedColorManager.py | 3 +- pyqtgraph/namedPen.py | 73 +++--- pyqtgraph/palette.py | 44 +++- pyqtgraph/widgets/GraphicsView.py | 30 ++- pyqtgraph/widgets/tests/test_graphics_view.py | 6 +- 16 files changed, 536 insertions(+), 174 deletions(-) create mode 100644 examples/PaletteApplicationExample.py create mode 100644 pyqtgraph/namedBrush.py diff --git a/examples/PaletteApplicationExample.py b/examples/PaletteApplicationExample.py new file mode 100644 index 00000000..1bf5025a --- /dev/null +++ b/examples/PaletteApplicationExample.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +Update a simple plot as rapidly as possible to measure speed. +""" + +## Add path to library (just for examples; you do not need this) +import initExample + + +import numpy as np + +import pyqtgraph as pg +from pyqtgraph.Qt import mkQApp, QtGui, QtCore, QtWidgets +from pyqtgraph.ptime import time +from pyqtgraph import functions as fn + + +class MainWindow(QtWidgets.QMainWindow): + """ example application main window """ + def __init__(self, *args, **kwargs): + super(MainWindow, self).__init__(*args, **kwargs) + + main_wid = QtWidgets.QWidget() + self.setCentralWidget(main_wid) + self.setWindowTitle('pyqtgraph example: Palette application test') + self.resize(600,600) + + # pg.functions.SIGNAL_SOURCE.paletteChangedSignal.connect(self.testSignal) # test link + # pg.palette.get('monogreen').apply() + + main_layout = QtWidgets.QGridLayout( main_wid ) + gr_wid = pg.GraphicsLayoutWidget(show=True) + main_layout.addWidget( gr_wid, 0,0, 1,4 ) + + btn = QtWidgets.QPushButton('continuous') + btn.clicked.connect(self.handle_button_timer_on) + main_layout.addWidget(btn, 1,0, 1,1 ) + + btn = QtWidgets.QPushButton('stop updates') + btn.clicked.connect(self.handle_button_timer_off) + main_layout.addWidget(btn, 2,0, 1,1 ) + + btn = QtWidgets.QPushButton('apply ') + btn.clicked.connect(self.handle_button_pal1) + main_layout.addWidget(btn, 1,2, 1,1 ) + + btn = QtWidgets.QPushButton('apply ') + btn.clicked.connect(self.handle_button_pal2) + main_layout.addWidget(btn, 1,3, 1,1 ) + + btn = QtWidgets.QPushButton('apply ') + btn.clicked.connect(self.handle_button_pal3) + main_layout.addWidget(btn, 2,2, 1,1 ) + + btn = QtWidgets.QPushButton('apply ') + btn.clicked.connect(self.handle_button_pal4) + main_layout.addWidget(btn, 2,3, 1,1 ) + + # self.manager = pg.fn.NAMED_COLOR_MANAGER + # self.pen1 = pg.NamedPen('p1') #,width=0.5) + + self.plt = gr_wid.addPlot() + self.plt.enableAutoRange(False) + self.plt.setYRange( -7,7 ) + self.plt.setXRange( 0, 15 ) #500 ) + + self.plt.setLabel('bottom', 'Index', units='B') + + self.data1 = +3 + np.random.normal(size=(15)) #500)) + self.data2 = -3 + np.random.normal(size=(15)) #500)) + + self.curve1 = pg.PlotDataItem(pen='r', symbol='o', symbolSize=10, symbolPen='gr_fg', symbolBrush=('y',127)) + self.plt.addItem(self.curve1) + + self.curve2 = pg.PlotCurveItem(pen='p3', brush='p4') + self.curve2.setFillLevel(0) + self.plt.addItem(self.curve2) + self.show() + + self.pal_1 = pg.palette.get('legacy') + self.pal_2 = pg.palette.get('monogreen') + self.pal_3 = pg.palette.get('relaxed_dark') + self.pal_4 = pg.palette.get('relaxed_light') + + self.lastTime = time() + self.fps = None + self.timer = QtCore.QTimer(singleShot=False) + self.timer.timeout.connect( self.timed_update ) + + self.timed_update() + + def testSignal(self, val): + """ demonstrate use of PaletteChanged signal """ + print('"Palette changed" signal was received with value', val) + + + def handle_button_timer_on(self): + """ (re-)activate timer """ + self.timer.start(1) + + def handle_button_timer_off(self): + """ de-activate timer """ + self.timer.stop() + + def handle_button_pal1(self): + """ apply palette 1 on request """ + print('--> legacy') + self.pal_1.apply() + + def handle_button_pal2(self): + """ apply palette 2 on request """ + print('--> mono green') + self.pal_2.apply() + + def handle_button_pal3(self): + """ apply palette 1 on request """ + print('--> relax(light)') + self.pal_3.apply() + + def handle_button_pal4(self): + """ apply palette 1 on request """ + print('--> relax(light)') + self.pal_4.apply() + + def timed_update(self): + """ update loop, called by timer """ + self.data1[:-1] = self.data1[1:] + self.data1[-1] = +3 + np.random.normal() + xdata = np.arange( len(self.data1) ) + self.curve1.setData( x=xdata, y=self.data1 ) + + self.data2[:-1] = self.data2[1:] + self.data2[-1] = -3 + np.random.normal() + self.curve2.setData( y=self.data2 ) + + now = time() + dt = now - self.lastTime + self.lastTime = now + if self.fps is None: + self.fps = 1.0/dt + else: + s = np.clip(dt*3., 0, 1) + self.fps = self.fps * (1-s) + (1.0/dt) * s + self.plt.setTitle('%0.2f fps' % self.fps) + QtWidgets.QApplication.processEvents() ## force complete redraw for every plot + +mkQApp("Palette test application") +main_window = MainWindow() + +## Start Qt event loop +if __name__ == '__main__': + QtWidgets.QApplication.instance().exec_() diff --git a/examples/utils.py b/examples/utils.py index a7ac8bd6..b6fcf05a 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -1,7 +1,6 @@ from collections import OrderedDict from argparse import Namespace - examples = OrderedDict([ ('Command-line usage', 'CLIexample.py'), ('Basic Plotting', Namespace(filename='Plotting.py', recommended=True)), @@ -21,6 +20,7 @@ examples = OrderedDict([ ('Auto-range', 'PlotAutoRange.py'), ('Remote Plotting', 'RemoteSpeedTest.py'), ('Scrolling plots', 'scrollingPlots.py'), + ('Palette adjustment','PaletteApplicationExample.py'), ('HDF5 big data', 'hdf5.py'), ('Demos', OrderedDict([ ('Optics', 'optics_demos.py'), diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index ea31720c..63f5219c 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -62,12 +62,17 @@ CONFIG_OPTIONS = { 'useCupy': False, # When True, attempt to use cupy ( currently only with ImageItem and related functions ) } - def setConfigOption(opt, value): if opt not in CONFIG_OPTIONS: raise KeyError('Unknown configuration option "%s"' % opt) if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'): raise ValueError('imageAxisOrder must be either "row-major" or "col-major"') + # setConfigOption should be relocated to have access to functions.py + # Then background / foreground updates can be intercepted and applied to the palette + # if opt == 'background': + # functions.Colors['gr_bg'] = functions.Colors[value] + # if opt == 'foreground': + # functions.Colors['gr_fg'] = functions.Colors[value] CONFIG_OPTIONS[opt] = value def setConfigOptions(**opts): @@ -280,6 +285,7 @@ from .ptime import time from .Qt import isQObjectAlive from .ThreadsafeTimer import * from .namedPen import * +from .namedBrush import * from .palette import * diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 10193301..4f7a9795 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -22,6 +22,7 @@ from .Qt import QtGui, QtCore, QT_LIB, QtVersion from . import Qt from .namedColorManager import NamedColorManager from .namedPen import NamedPen +from .namedBrush import NamedBrush from .metaarray import MetaArray from collections import OrderedDict @@ -219,25 +220,66 @@ class Color(QtGui.QColor): return (self.red, self.green, self.blue, self.alpha)[ind]() +def parseNamedColorSpecification(*args): + """ + check if args specify a NamedColor, looking for + 'name' or ('name', alpha) information. + Returns: + None if invalid + ('name', alpha) if a valid name and alpha value is given + ('name', None) if no alpha value is available + ('', None) if an empty name is given, indicating a blank color + None if the specification does not match a NamedColor + """ + while len(args) <= 1: + if len(args) == 0: + return None + if len(args) == 1: + arg = args[0] + if isinstance(arg, str): + if len(arg) == 0: + return ('', None) # valid, but blank + if arg[0] == '#': + return None # hexadecimal string not handled as NamedColor + if arg in Colors: + return (arg, None) # valid name, no alpha given + if isinstance(arg, tuple) or isinstance(arg, list): + args = arg # promote to top level + else: + return None #numerical values not handled as NamedColor + if len(args) == 2: + if isinstance(arg[0], str): + return (arg[0], arg[1]) # return ('name', alpha) tuple + return None # all other cases not handled as NamedColor + + def mkColor(*args): """ Convenience function for constructing QColor from a variety of argument types. Accepted arguments are: ================ ================================================ - 'c' one of: r, g, b, c, m, y, k, w + 'name' any color name specifed in palette R, G, B, [A] integers 0-255 (R, G, B, [A]) tuple of integers 0-255 float greyscale, 0.0-1.0 int see :func:`intColor() ` (int, hues) see :func:`intColor() ` - "RGB" hexadecimal strings; may begin with '#' - "RGBA" - "RRGGBB" - "RRGGBBAA" + "#RGB" hexadecimal strings; should begin with '#' + "#RGBA" + "#RRGGBB" + "#RRGGBBAA" QColor QColor instance; makes a copy. ================ ================================================ """ err = 'Not sure how to make a color from "%s"' % str(args) + result = parseNamedColorSpecification(args) # check if this is a named palette color + if result is not None: # make a return palette color + name, alpha = result + if name == '': + return QtGui.QColor(0,0,0,0) # empty string means "no color" + qcol = Colors[name] + if alpha is not None: qcol.setAlpha( alpha ) + return qcol if len(args) == 1: if isinstance(args[0], basestring): c = args[0] @@ -301,116 +343,115 @@ def mkColor(*args): return QtGui.QColor(*args) -def mkBrush(*args, **kwds): +def mkBrush(*args, **kargs): """ | Convenience function for constructing Brush. | This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() ` | Calling mkBrush(None) returns an invisible brush. """ - print('mkBrush called with',args,kwds) - if 'color' in kwds: - color = kwds['color'] - elif len(args) == 1: - arg = args[0] - if arg is None: - return QtGui.QBrush(QtCore.Qt.NoBrush) - elif isinstance(arg, QtGui.QBrush): - return QtGui.QBrush(arg) + while ( # unravel single element sublists + ( isinstance(args, tuple) or isinstance(args,list) ) + and len(args) == 1 + ): + args = args[0] + # now args is either a non-list entity, or a multi-element tuple + # short-circuits: + if isinstance(args, NamedBrush): + return args # pass through predefined NamedPen directly + elif isinstance(args, QtGui.QBrush): + return QtGui.QBrush(args) ## return a copy of this brush + elif isinstance(args, dict): + return mkBrush(**args) # retry with kwargs assigned from dictionary + elif args is None: + return QtGui.QBrush( QtCore.Qt.NoBrush ) # explicit None means "no brush" + # no short-circuit, continue parsing to construct QPen or NamedPen + if 'hsv' in kargs: # hsv argument takes precedence + qcol = hsvColor( *kargs['hsv'] ) + qbrush = QtGui.QBrush(qcol) + else: + if 'color' in kargs: + args = kargs['color'] # 'color' KW-argument overrides unnamed arguments + if args is None: + return QtGui.QBrush( QtCore.Qt.NoBrush ) # explicit None means "no brush" + if args == () or args == []: + print(' functions: returning default color NamedBrush') + qpen = NamedBrush( 'gr_fg' ) # default foreground color else: - color = arg - elif len(args) > 1: - color = args - return QtGui.QBrush(mkColor(color)) - -def mkNamedPen(name, **kargs): - """ - Try to create a named pen. - Currently, this quietly returns None if 'name' does not specify a color. - This causes mkPen to keep trying to parse color information. - - In addition to arguments 'style', 'width', 'dash' and 'cosmetic', - 'alpha' = 0-255 sets the opacity of the named pen - """ - alpha = kargs.get('alpha', None) - try: - pen = NamedPen( name, alpha=alpha ) - except ValueError: - print(' failed to make NamedPen',name,kargs) - return None - if kargs == {}: - print(' prepared NamedPen',name,'(no kargs)') - return pen # default pen of width zero is cosmetic by default - style = kargs.get('style', None) # in many cases, a predefined pen is just passed through - width = kargs.get('width', 1) # collect remaining kargs to define properties - dash = kargs.get('dash', None) - cosmetic = kargs.get('cosmetic', True) - - pen.setCosmetic(cosmetic) - if style is not None: pen.setStyle(style) - if dash is not None: pen.setDashPattern(dash) - print(' prepared NamedPen',name,kargs) - return pen - + result = parseNamedColorSpecification(args) + if result is not None: # make a NamedBrush + name, alpha = result + if name == '': + return QtGui.QBrush( QtCore.Qt.NoBrush ) # empty string means "no brush" + qbrush = NamedBrush(name, alpha=alpha) + else: # make a QBrush + qcol = mkColor(args) + qbrush = QtGui.QBrush(qcol) + # here we would apply additional style based on KW-arguments + return qbrush def mkPen(*args, **kargs): """ Convenience function for constructing QPen. Examples:: - mkPen(color) mkPen(color, width=2) mkPen(cosmetic=False, width=4.5, color='r') mkPen({'color': "FF0", width: 2}) mkPen(None) # (no pen) + mkPen() # default color In these examples, *color* may be replaced with any arguments accepted by :func:`mkColor() ` """ - # print('mkPen called:',args,kargs) - color = kargs.get('color', None) # collect only immediately required properties - style = kargs.get('style', None) # in many cases, a predefined pen is just passed through - - pen = None - if len(args) == 1: - arg = args[0] - if isinstance(arg, str): - if len(arg) == 0: # empty string sets "no pen" - style = QtCore.Qt.NoPen - elif arg[0] != '#': - pen = mkNamedPen(arg) - elif isinstance(arg, NamedPen): - return arg # pass through predefined NamedPen directly - elif isinstance(arg, QtGui.QPen): - return QtGui.QPen(arg) ## return a copy of this pen - elif isinstance(arg, dict): - return mkPen(**arg) - elif arg is None: - style = QtCore.Qt.NoPen + while ( # unravel single element sublists + ( isinstance(args, tuple) or isinstance(args,list) ) + and len(args) == 1 + ): + args = args[0] + # now args is either a non-list entity, or a multi-element tuple + # short-circuits: + if isinstance(args, NamedPen): + return args # pass through predefined NamedPen directly + elif isinstance(args, QtGui.QPen): + return QtGui.QPen(args) ## return a copy of this pen + elif isinstance(args, dict): + return mkPen(**args) # retry with kwargs assigned from dictionary + elif args is None: + return QtGui.QPen( QtCore.Qt.NoPen ) # explicit None means "no pen" + # no short-circuit, continue parsing to construct QPen or NamedPen + width = kargs.get('width', 1) # width 1 unless specified otherwise + if 'hsv' in kargs: # hsv argument takes precedence + qcol = hsvColor( *kargs['hsv'] ) + qpen = QtGui.QPen(QtGui.QBrush(qcol), width) + else: + if 'color' in kargs: + args = kargs['color'] # 'color' KW-argument overrides unnamed arguments + if args is None: + return QtGui.QPen( QtCore.Qt.NoPen ) # explicit None means "no pen" + if args == () or args == []: + qpen = NamedPen( 'gr_fg', width=width ) # default foreground color else: - color = arg - elif len(args) > 1: - color = args - + result = parseNamedColorSpecification(args) + if result is not None: # make a NamedPen + name, alpha = result + if name == '': + return QtGui.QPen( QtCore.Qt.NoPen ) # empty string means "no pen" + qpen = NamedPen( name, alpha=alpha, width=width ) + else: # make a QPen + qcol = mkColor(args) + qpen = QtGui.QPen(QtGui.QBrush(qcol), width) + # now apply styles according to kw arguments: + style = kargs.get('style', None) width = kargs.get('width', 1) # collect remaining kargs to define properties dash = kargs.get('dash', None) cosmetic = kargs.get('cosmetic', True) - hsv = kargs.get('hsv', None) - - if color is None: - color = mkColor('l') - if hsv is not None: - color = hsvColor(*hsv) - else: - color = mkColor(color) - - if pen is None: - pen = QtGui.QPen(QtGui.QBrush(color), width) - pen.setCosmetic(cosmetic) + assert qpen is not None + qpen.setCosmetic(cosmetic) if style is not None: - pen.setStyle(style) + qpen.setStyle(style) if dash is not None: - pen.setDashPattern(dash) - return pen + qpen.setDashPattern(dash) + return qpen def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 6dd75a4c..e3ef1066 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -104,7 +104,7 @@ class AxisItem(GraphicsWidget): self.setPen(pen) if textPen is None: - self.setTextPen('gr_fg') # default foreground color + self.setTextPen('gr_txt') # default text color else: self.setTextPen(pen) @@ -1217,4 +1217,6 @@ class AxisItem(GraphicsWidget): def styleHasChanged(self): """ self.picture needs to be invalidated to initiate full redraw """ self.picture = None + self.labelStyle['color'] = self._textPen.color().name() + self._updateLabel() super().styleHasChanged() diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py index 0a656a77..f0fde5eb 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -5,6 +5,7 @@ from .GraphicsItem import GraphicsItem from .. import functions as fn __all__ = ['GraphicsObject'] +DEBUG = False try: # prepare common definition for slot decorator across PyQt / Pyside: QT_CORE_SLOT = QtCore.pyqtSlot @@ -58,4 +59,4 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): """ called to trigger redraw after all named colors have been updated """ # self._boundingRect = None self.update() - print('redraw after style change:', self) \ No newline at end of file + if DEBUG: print(' GrpahicsObject: redraw after style change:', self) \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/GraphicsWidget.py b/pyqtgraph/graphicsItems/GraphicsWidget.py index 9bec44a0..6df50962 100644 --- a/pyqtgraph/graphicsItems/GraphicsWidget.py +++ b/pyqtgraph/graphicsItems/GraphicsWidget.py @@ -4,6 +4,7 @@ from .GraphicsItem import GraphicsItem from .. import functions as fn __all__ = ['GraphicsWidget'] +DEBUG = False try: # prepare common definition for slot decorator across PyQt / Pyside: QT_CORE_SLOT = QtCore.pyqtSlot @@ -22,7 +23,6 @@ class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget): """ QtGui.QGraphicsWidget.__init__(self, *args, **kargs) GraphicsItem.__init__(self) - # fn.NAMED_COLOR_MANAGER.paletteChangeSignal.connect(self.styleChange) fn.NAMED_COLOR_MANAGER.paletteHasChangedSignal.connect(self.styleHasChanged) ## done by GraphicsItem init @@ -64,16 +64,9 @@ class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget): #print "shape:", p.boundingRect() return p - # @QT_CORE_SLOT(dict) - # # @QtCore.Slot() - # def styleChange(self, color_dict): - # """ stub function called after Palette.apply(), specific ractions to palette redefinitions execute here """ - # print('style change request:', self, type(color_dict)) - @QT_CORE_SLOT() - # @QtCore.Slot(dict) def styleHasChanged(self): """ called to trigger redraw after all named colors have been updated """ # self._boundingRect = None self.update() - print('redraw after style change:', self) + if DEBUG: print(' GraphicsWidget: redraw after style change:', self) diff --git a/pyqtgraph/graphicsItems/LabelItem.py b/pyqtgraph/graphicsItems/LabelItem.py index ba8b72fe..9014d355 100644 --- a/pyqtgraph/graphicsItems/LabelItem.py +++ b/pyqtgraph/graphicsItems/LabelItem.py @@ -15,16 +15,19 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor): Note: To display text inside a scaled view (ViewBox, PlotWidget, etc) use TextItem """ - def __init__(self, text=' ', parent=None, angle=0, **args): GraphicsWidget.__init__(self, parent) GraphicsWidgetAnchor.__init__(self) self.item = QtGui.QGraphicsTextItem(self) self.opts = { - 'color': None, + 'color': 'gr_txt', # default text color. Was: None, 'justify': 'center' } self.opts.update(args) + self._brush = fn.mkBrush(self.opts['color']) # make a NamedBrush by default + self._hex_color_override = None + self._optlist = [] + self._sizeHint = {} self.setText(text) self.setAngle(angle) @@ -32,6 +35,9 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor): def setAttr(self, attr, value): """Set default text properties. See setText() for accepted parameters.""" self.opts[attr] = value + if attr == 'color': + self._brush = fn.mkBrush(value) # make a new NamedBrush/QBrush + self._hex_color_override = None # to use for all normal output def setText(self, text, **args): """Set the text and text properties in the label. Accepts optional arguments for auto-generating @@ -46,25 +52,28 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor): ==================== ============================== """ self.text = text + if 'color' in args: + # temporary override for color: + col = fn.mkColor(opts['color']) + self._hex_color_override = col.name() + color_opt = self._hex_color_override + else: + self._hex_color_override = None # return to defined color + color_opt = self._brush.color().name() + opts = self.opts for k in args: opts[k] = args[k] - - optlist = [] - - color = self.opts['color'] - if color is None: - color = getConfigOption('foreground') - color = fn.mkColor(color) - optlist.append('color: #' + fn.colorStr(color)[:6]) + + self.optlist = [] + # self.optlist.append('color: ' + col.name() if 'size' in opts: - optlist.append('font-size: ' + opts['size']) + self.optlist.append('font-size: ' + opts['size']) if 'bold' in opts and opts['bold'] in [True, False]: - optlist.append('font-weight: ' + {True:'bold', False:'normal'}[opts['bold']]) + self.optlist.append('font-weight: ' + {True:'bold', False:'normal'}[opts['bold']]) if 'italic' in opts and opts['italic'] in [True, False]: - optlist.append('font-style: ' + {True:'italic', False:'normal'}[opts['italic']]) - full = "%s" % ('; '.join(optlist), text) - #print full + self.optlist.append('font-style: ' + {True:'italic', False:'normal'}[opts['italic']]) + full = "{:s}".format(color_opt, '; '.join(self.optlist), self.text) self.item.setHtml(full) self.updateMin() self.resizeEvent(None) @@ -133,10 +142,18 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor): def itemRect(self): return self.item.mapRectToParent(self.item.boundingRect()) - + + def styleHasChanged(self): + """ overridden to update color without changing the text """ + if self._hex_color_override is not None: + return # nothing to do, overridden text color will not change. + color_opt = self._brush.color().name() # get updated color + full = "{:s}".format(color_opt, '; '.join(self.optlist), self.text) + self.item.setHtml(full) + super().styleHasChanged() + #def paint(self, p, *args): #p.setPen(fn.mkPen('r')) #p.drawRect(self.rect()) #p.setPen(fn.mkPen('g')) #p.drawRect(self.itemRect()) - diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 0004773d..40627811 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -20,6 +20,7 @@ from ..python2_3 import basestring __all__ = ['ScatterPlotItem', 'SpotItem'] +DEBUG = False # When pxMode=True for ScatterPlotItem, QPainter.drawPixmap is used for drawing, which @@ -253,6 +254,7 @@ class SymbolAtlas(object): images = [] data = [] for key, style in styles.items(): + if DEBUG: print('\nrender:', style[2].color().name(), style[3].color().name(), style) img = renderSymbol(*style) arr = fn.imageToArray(img, copy=False, transpose=False) images.append(img) # keep these to delay garbage collection @@ -411,8 +413,8 @@ class ScatterPlotItem(GraphicsObject): 'name': None, 'symbol': 'o', 'size': 7, - 'pen': fn.mkPen(getConfigOption('foreground')), - 'brush': fn.mkBrush(100, 100, 150), + 'pen': fn.mkPen('gr_fg'), # getConfigOption('foreground')), + 'brush': fn.mkBrush(('gr_fg',128)), # (100, 100, 150), 'hoverable': False, 'tip': 'x: {x:.3g}\ny: {y:.3g}\ndata={data}'.format, } @@ -489,7 +491,6 @@ class ScatterPlotItem(GraphicsObject): Add new points to the scatter plot. Arguments are the same as setData() """ - ## deal with non-keyword arguments if len(args) == 1: kargs['spots'] = args[0] @@ -1235,6 +1236,19 @@ class ScatterPlotItem(GraphicsObject): def _hasHoverStyle(self): return any(self.opts['hover' + opt.title()] != _DEFAULT_STYLE[opt] for opt in ['symbol', 'size', 'pen', 'brush']) + + def styleHasChanged(self): + """ overridden to trigger symbol atlas refresh """ + if DEBUG: + print(' ScatterPlotItem: style update!') + print(' pens:',self.data['pen'] ) + print(' default pen :', self.opts[ 'pen' ].color().name(), self.opts[ 'pen' ] ) + print(' default brush:', self.opts['brush'].color().name(), self.opts['brush'] ) + self.fragmentAtlas.clear() + self.data['sourceRect'] = (0, 0, 0, 0) + self.updateSpots(self.data) + super().styleHasChanged() + class SpotItem(object): diff --git a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py index 3eb70271..095843e6 100644 --- a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py @@ -87,7 +87,8 @@ def test_init_spots(): # check data is correct spots = s.points() - defPen = pg.mkPen(pg.getConfigOption('foreground')) + # defPen = pg.mkPen(pg.getConfigOption('foreground')) + defPen = pg.mkPen('gr_fg') assert spots[0].pos().x() == 0 assert spots[0].pos().y() == 1 diff --git a/pyqtgraph/namedBrush.py b/pyqtgraph/namedBrush.py new file mode 100644 index 00000000..22896405 --- /dev/null +++ b/pyqtgraph/namedBrush.py @@ -0,0 +1,80 @@ +# from ..Qt import QtGui +from .Qt import QtGui, QtCore + +from . import functions as fn + +__all__ = ['NamedBrush'] +DEBUG = False + +class NamedBrush(QtGui.QBrush): + """ Extends QPen to retain a functional color description """ + def __init__(self, name, alpha=None ): + """ + Creates a new NamedBrush object. + 'name' should be in 'functions.Colors' + 'alpha' controls opacity which persists over palette changes + """ + if DEBUG: print(' NamedBrush created as',name,alpha) + super().__init__(QtCore.Qt.SolidPattern) # Initialize QBrush superclass + self._identifier = (name, alpha) + self._updateQColor(self._identifier) + fn.NAMED_COLOR_MANAGER.register( self ) # manually register for callbacks + + def __eq__(self, other): # make this a hashable object + # return other is self + if isinstance(other, self.__class__): + return self._identifier == other._identifier + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return id(self) + + def setColor(self, name=None, alpha=None): + """ update color name. This does not trigger a global redraw. """ + if name is None: + name = self._identifier[0] + elif isinstance(name, QtGui.QColor): + # Replicates alpha adjustment workaround in NamedPen, allowing only alpha to be adjusted retroactively + if alpha is None: + alpha = name.alpha() # extract from given QColor + name = self._identifier[0] + if DEBUG: print(' NamedBrush: setColor(QColor) call: set alpha to', alpha) + self._identifier = (name, alpha) + self._updateQColor(self._identifier) + + def setAlpha(self, alpha): + """ update opacity value """ + self._identifier = (self._identifier[0], alpha) + self._updateQColor(self._identifier) + + def _updateQColor(self, identifier, color_dict=None): + """ update super-class QColor """ + name, alpha = identifier + if color_dict is None: + color_dict = fn.NAMED_COLOR_MANAGER.colors() + try: + qcol = fn.Colors[name] + except ValueError as exc: + raise ValueError("Color '{:s}' is not in list of defined colors".format(str(name)) ) from exc + if alpha is not None: + qcol.setAlpha( alpha ) + if DEBUG: print(' NamedBrush '+name+' updated to QColor ('+str(qcol.name())+', alpha='+str(alpha)+')') + super().setColor( qcol ) + + def identifier(self): + """ return current color identifier """ + return self._identifier + + def paletteChange(self, color_dict): + """ refresh QColor according to lookup of identifier in functions.Colors """ + if DEBUG: print(' NamedBrush: style change request:', self, type(color_dict)) + self._updateQColor(self._identifier, color_dict=color_dict) + if DEBUG: + qcol = super().color() + name, alpha = self._identifier + print(' NamedBrush: retrieved new QColor ('+str(qcol.name())+') ' + + 'for name '+str(name)+' ('+str(alpha)+')' ) diff --git a/pyqtgraph/namedColorManager.py b/pyqtgraph/namedColorManager.py index d3957483..f9447c05 100644 --- a/pyqtgraph/namedColorManager.py +++ b/pyqtgraph/namedColorManager.py @@ -34,7 +34,7 @@ for idx, col in enumerate( ( # twelve predefined plot colors ) ): key = 'p{:X}'.format(idx) DEFAULT_COLORS[key] = DEFAULT_COLORS[col] - + # define and instantiate a SignalSource object to pass signals to all pyqtgraph elements class NamedColorManager(QtCore.QObject): # this needs to emit QEvents """ @@ -64,6 +64,7 @@ class NamedColorManager(QtCore.QObject): # this needs to emit QEvents def register(self, obj): """ register a function for paletteChange callback """ self.registered_objects.add( obj ) + # if DEBUG: print(' NamedColorManager: New list', self.registered_objects ) def redefinePalette(self, color_dic): """ update list of named colors, emitsignals to color objects and widgets """ diff --git a/pyqtgraph/namedPen.py b/pyqtgraph/namedPen.py index 5185a528..195c0809 100644 --- a/pyqtgraph/namedPen.py +++ b/pyqtgraph/namedPen.py @@ -11,42 +11,31 @@ class NamedPen(QtGui.QPen): def __init__(self, name, width=1, alpha=None ): """ Creates a new NamedPen object. - 'identifier' should be a 'name' included in 'functions.Colors' or - '(name, alpha)' to include transparency + 'name' should be included in 'functions.Colors' + 'width' specifies linewidth and defaults to 1 + 'alpha' controls opacity which persists over palette changes """ - try: - qcol = fn.Colors[name] - # print('QColor alpha is', qcol.alpha() ) - except ValueError as exc: - raise ValueError("Color {:s} is not in list of defined colors".format(str(name)) ) from exc - if alpha is not None: - if DEBUG: print(' NamedPen: setting alpha to',alpha) - qcol.setAlpha( alpha ) - - super().__init__( QtGui.QBrush(qcol), width) # Initialize QPen superclass + if DEBUG: print(' NamedBrush created as',name,alpha) + super().__init__(QtCore.Qt.SolidLine) # Initialize QPen superclass + super().setWidth(width) super().setCosmetic(True) self._identifier = (name, alpha) + self._updateQColor(self._identifier) fn.NAMED_COLOR_MANAGER.register( self ) # manually register for callbacks def __eq__(self, other): # make this a hashable object - return other is self + # return other is self + if isinstance(other, self.__class__): + return self._identifier == other._identifier + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + def __hash__(self): return id(self) - # def _parse_identifier(self, identifier): - # """ parse identifier parameter, which can be 'name' or '(name, alpha)' """ - # alpha = None - # if isinstance(identifier, str): - # name = identifier - # else: - # try: - # name, alpha = identifier - # except ValueError as exc: - # raise ValueError("Invalid argument. 'identifier' should be 'name' or ('name', 'alpha'), but is {:s}".format(str(color)) ) from exc - # if name[0] == '#': - # raise TypeError("NamedPen should not be used for fixed colors ('name' = {:s} was given)".format(str(name)) ) - # return name, alpha - def setColor(self, name=None, alpha=None): """ update color name. This does not trigger a global redraw. """ if name is None: @@ -54,23 +43,33 @@ class NamedPen(QtGui.QPen): elif isinstance(name, QtGui.QColor): # this is a workaround for the alpha adjustements in AxisItem: # While the color will not change, the alpha value can be adjusted as needed. - alpha = name.alpha() # extract - self._identifier = self._identifier[0], alpha - # print(' NamedColor setColor(QColor) call: set alpha to', name.alpha()) + if alpha is None: + alpha = name.alpha() # extract from given QColor name = self._identifier[0] + if DEBUG: print(' NamedPen: setColor(QColor) call: set alpha to', alpha) + self._identifier = (name, alpha) + self._updateQColor(self._identifier) + + def setAlpha(self, alpha): + """ update opacity value """ + self._identifier = (self._identifier[0], alpha) + self._updateQColor(self._identifier) + + def _updateQColor(self, identifier, color_dict=None): + """ update super-class QColor """ + name, alpha = identifier + if color_dict is None: + color_dict = fn.NAMED_COLOR_MANAGER.colors() try: qcol = fn.Colors[name] except ValueError as exc: - raise ValueError("Color {:s} is not in list of defined colors".format(str(name)) ) from exc - if alpha is not None: + raise ValueError("Color '{:s}' is not in list of defined colors".format(str(name)) ) from exc + if alpha is not None: qcol.setAlpha( alpha ) + if DEBUG: print(' NamedPen updated to QColor ('+str(qcol.name())+')') super().setColor( qcol ) - self._identifier = (name, alpha) - - # def setBrush(self): - # """ disabled """ - # return None + def paletteChange(self, color_dict): """ refresh QColor according to lookup of identifier in functions.Colors """ diff --git a/pyqtgraph/palette.py b/pyqtgraph/palette.py index 5d7c0c55..f1b97451 100644 --- a/pyqtgraph/palette.py +++ b/pyqtgraph/palette.py @@ -30,12 +30,12 @@ LEGACY_PLOT = [ # plot / accent colors: ] MONOGREEN_RAW = { - 'col_g0':'#000000', 'col_g1':'#014801', 'col_g2':'#077110', 'col_g3':'#159326', + 'col_g0':'#001000', 'col_g1':'#014801', 'col_g2':'#077110', 'col_g3':'#159326', 'col_g4':'#2DB143', 'col_g5':'#50CD65', 'col_g6':'#7FE7A0', 'col_g7':'#BFFFD4' } MONOGREEN_FUNC = { 'gr_fg' : 'col_g5', - 'gr_bg' : 'col_g1', # for distinction in testing, should be col_g0 + 'gr_bg' : 'col_g0', # for distinction in testing, should be col_g0 'gr_txt' : 'col_g5', 'gr_acc' : 'col_g5', 'gr_hov' : 'col_g7', @@ -64,11 +64,11 @@ RELAXED_RAW = { # "fresh" raw colors: 'col_grass' :'#7AA621', 'col_l_grass' :'#BCD982', 'col_d_grass' :'#50730B', 'col_yellow':'#BFB226', 'col_l_yellow':'#F2E985', 'col_d_yellow':'#80760D', 'col_gold' :'#A67A21', 'col_l_gold' :'#D9B46C', 'col_d_gold' :'#73500B', - 'col_black' :'#000000', 'col_gr1' :'#242429', 'col_gr2' :'#44444D', - 'col_gr3' :'#575763', 'col_gr4' :'#7B7B8C', 'col_gr5' :'#B4B4CC', + # 'col_black' :'#000000', 'col_gr1' :'#242429', 'col_gr2' :'#44444D', + 'col_black' :'#000000', 'col_gr1' :'#161619', 'col_gr2' :'#43434D', + 'col_gr3' :'#70707F', 'col_gr4' :'#9D9DB2', 'col_gr5' :'#C9C9E5', 'col_white' :'#FFFFFF' } -# 'col_gray' :'#666666', 'col_l_gray' :'#B6B6B6', 'col_d_gray' :'#3D3D3D', RELAXED_DARK_FUNC= { # functional colors: 'gr_fg' : 'col_gr5', 'gr_bg' : 'col_gr1', @@ -97,6 +97,36 @@ RELAXED_DARK_PLOT = [ # plot / accent colors: 'col_l_green' ] +RELAXED_LIGHT_FUNC= { # functional colors: + 'gr_fg' : 'col_gr1', + 'gr_bg' : 'col_gr5', + 'gr_txt' : 'col_black', + 'gr_acc' : 'col_orange', + 'gr_hov' : 'col_black', + 'gr_reg' : ('col_blue', 30), + # legacy colors: + 'b': 'col_blue' , 'c': 'col_cyan', 'g': 'col_green', + 'y': 'col_yellow', 'r': 'col_red' , 'm': 'col_violet', + 'k': 'col_black' , 'w': 'col_white', + 'd': 'col_gr2' , 'l': 'col_gr4' , 's': 'col_sky' +} +RELAXED_LIGHT_PLOT = [ # plot / accent colors: + 'col_sky' , + 'col_indigo', + 'col_purple', + 'col_red' , + 'col_gold' , + 'col_grass' , + 'col_cyan' , + 'col_blue' , + 'col_violet', + 'col_orange', + 'col_yellow', + 'col_green' +] + + + def block_to_QColor( block, dic=None ): """ convert color information to a QColor """ # allowed formats: @@ -152,8 +182,8 @@ DEFAULT_PALETTE = assemble_palette( LEGACY_RAW, LEGACY_FUNC, LEGACY_PLOT ) def get(name): if name == 'relaxed_dark': pal = assemble_palette( RELAXED_RAW, RELAXED_DARK_FUNC, RELAXED_DARK_PLOT ) - # elif name == 'relaxed_light': - # pal = assemble_palette( RELAXED_RAW, RELAXED_LIGHT_FUNC, RELAXED_LIGHT_PLOT ) + elif name == 'relaxed_light': + pal = assemble_palette( RELAXED_RAW, RELAXED_LIGHT_FUNC, RELAXED_LIGHT_PLOT ) elif name == 'monogreen': pal = assemble_palette( MONOGREEN_RAW, MONOGREEN_FUNC, MONOGREEN_PLOT ) else: diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index f72f7862..f543a30a 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -15,8 +15,15 @@ from .. import functions as fn from .. import debug as debug from .. import getConfigOption +try: # prepare common definition for slot decorator across PyQt / Pyside: + QT_CORE_SLOT = QtCore.pyqtSlot +except AttributeError: + QT_CORE_SLOT = QtCore.Slot + __all__ = ['GraphicsView'] +DEBUG = False + class GraphicsView(QtGui.QGraphicsView): """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the @@ -93,7 +100,6 @@ class GraphicsView(QtGui.QGraphicsView): self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter) self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate) - self.lockedViewports = [] self.lastMousePos = None self.setMouseTracking(True) @@ -123,6 +129,10 @@ class GraphicsView(QtGui.QGraphicsView): self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) self.clickAccepted = False + # connect to style update signals from NamedColorManager: + fn.NAMED_COLOR_MANAGER.paletteHasChangedSignal.connect(self.styleHasChanged) + + def setAntialiasing(self, aa): """Enable or disable default antialiasing. Note that this will only affect items that do not specify their own antialiasing options.""" @@ -137,12 +147,17 @@ class GraphicsView(QtGui.QGraphicsView): To use the defaults specified py pyqtgraph.setConfigOption, use background='default'. To make the background transparent, use background=None. """ - self._background = background if background == 'default': # background = getConfigOption('background') background = 'gr_bg' # default graphics background color - brush = fn.mkBrush(background) - self.setBackgroundBrush(brush) + self._background = background # maintained for compatibility + if DEBUG: print(' GraphicsView: Generating BG brush for', self._background) + self._bgBrush = fn.mkBrush(self._background) + if DEBUG: print(' GraphicsView: Background color: ',self._bgBrush.color().name(), self._bgBrush.color().alpha()) + self.setBackgroundBrush( self._bgBrush ) + # testBrush = QtGui.QBrush( QtGui.QColor('#000000') ) + # print(' test brush style:',testBrush.style() ) + # self.setBackgroundBrush( testBrush ) def paintEvent(self, ev): self.scene().prepareForPaint() @@ -402,3 +417,10 @@ class GraphicsView(QtGui.QGraphicsView): def dragEnterEvent(self, ev): ev.ignore() ## not sure why, but for some reason this class likes to consume drag events + + @QT_CORE_SLOT() + def styleHasChanged(self): + """ called to trigger redraw after all named colors have been updated """ + self.setBackgroundBrush( self._bgBrush ) + # self.update() + if DEBUG: print(' Background update and redraw after style change:', self) diff --git a/pyqtgraph/widgets/tests/test_graphics_view.py b/pyqtgraph/widgets/tests/test_graphics_view.py index 0871ee63..1992c879 100644 --- a/pyqtgraph/widgets/tests/test_graphics_view.py +++ b/pyqtgraph/widgets/tests/test_graphics_view.py @@ -27,13 +27,15 @@ def test_basics_graphics_view(): assert view.scaleCenter is False assert view.clickAccepted is False assert view.centralWidget is not None - assert view._background == "default" + # assert view._background == "default" + assert view._background == "gr_bg" # Set background color # -------------------------------------- view.setBackground("w") assert view._background == "w" - assert view.backgroundBrush().color() == QtCore.Qt.white + # assert view.backgroundBrush().color() == QtCore.Qt.white + assert view.backgroundBrush().color().name() == '#ffffff' #QtCore.Qt.white # Set anti aliasing # --------------------------------------