source: sasview/src/sas/qtgui/Plotting/Plotter.py @ eb1a386

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since eb1a386 was eb1a386, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Fixed text add functionality on plots - SASVIEW-859

  • Property mode set to 100644
File size: 24.4 KB
Line 
1from PyQt5 import QtCore
2from PyQt5 import QtGui
3from PyQt5 import QtWidgets
4
5import functools
6import copy
7import matplotlib.pyplot as plt
8from matplotlib.font_manager import FontProperties
9from sas.qtgui.Plotting.PlotterData import Data1D
10from sas.qtgui.Plotting.PlotterBase import PlotterBase
11from sas.qtgui.Plotting.AddText import AddText
12from sas.qtgui.Plotting.SetGraphRange import SetGraphRange
13from sas.qtgui.Plotting.LinearFit import LinearFit
14from sas.qtgui.Plotting.PlotProperties import PlotProperties
15
16import sas.qtgui.Utilities.GuiUtils as GuiUtils
17import sas.qtgui.Plotting.PlotUtilities as PlotUtilities
18
19class PlotterWidget(PlotterBase):
20    """
21    1D Plot widget for use with a QDialog
22    """
23    def __init__(self, parent=None, manager=None, quickplot=False):
24        super(PlotterWidget, self).__init__(parent, manager=manager, quickplot=quickplot)
25
26        self.parent = parent
27
28        # Dictionary of {plot_id:Data1d}
29        self.plot_dict = {}
30
31        # Window for text add
32        self.addText = AddText(self)
33
34        # Log-ness of the axes
35        self.xLogLabel = "log10(x)"
36        self.yLogLabel = "log10(y)"
37
38        # Data container for the linear fit
39        self.fit_result = Data1D(x=[], y=[], dy=None)
40        self.fit_result.symbol = 13
41        self.fit_result.name = "Fit"
42
43    @property
44    def data(self):
45        return self._data
46
47    @data.setter
48    def data(self, value):
49        """ data setter """
50        self._data = value
51        if value._xunit:
52            self.xLabel = "%s(%s)"%(value._xaxis, value._xunit)
53        else:
54            self.xLabel = "%s"%(value._xaxis)
55        if value._yunit:
56            self.yLabel = "%s(%s)"%(value._yaxis, value._yunit)
57        else:
58            self.yLabel = "%s"%(value._yaxis)
59
60        if value.scale == 'linear' or value.isSesans:
61            self.xscale = 'linear'
62            self.yscale = 'linear'
63        self.title(title=value.name)
64
65    def plot(self, data=None, color=None, marker=None, hide_error=False):
66        """
67        Add a new plot of self._data to the chart.
68        """
69        # Data1D
70        if isinstance(data, Data1D):
71            self.data = data
72        assert(self._data)
73
74        is_fit = (self.data.id=="fit")
75
76        # Transform data if required.
77        # TODO: it properly!
78        #if data.xtransform is not None or data.ytransform is not None:
79        #    a, b, c, d = GuiUtils.xyTransform(self.data, self.data.xtransform, self.data.ytransform)
80
81        # Shortcuts
82        ax = self.ax
83        x = self._data.view.x
84        y = self._data.view.y
85
86        # Marker symbol. Passed marker is one of matplotlib.markers characters
87        # Alternatively, picked up from Data1D as an int index of PlotUtilities.SHAPES dict
88        if marker is None:
89            marker = self.data.symbol
90            # Try name first
91            try:
92                marker = dict(PlotUtilities.SHAPES)[marker]
93            except KeyError:
94                marker = list(PlotUtilities.SHAPES.values())[marker]
95
96        assert marker is not None
97        # Plot name
98        if self.data.title:
99            self.title(title=self.data.title)
100        else:
101            self.title(title=self.data.name)
102
103        # Error marker toggle
104        if hide_error is None:
105            hide_error = self.data.hide_error
106
107        # Plot color
108        if color is None:
109            color = self.data.custom_color
110
111        color = PlotUtilities.getValidColor(color)
112
113        markersize = self._data.markersize
114
115        # Draw non-standard markers
116        l_width = markersize * 0.4
117        if marker == '-' or marker == '--':
118            line = self.ax.plot(x, y, color=color, lw=l_width, marker='',
119                             linestyle=marker, label=self._title, zorder=10)[0]
120
121        elif marker == 'vline':
122            y_min = min(y)*9.0/10.0 if min(y) < 0 else 0.0
123            line = self.ax.vlines(x=x, ymin=y_min, ymax=y, color=color,
124                            linestyle='-', label=self._title, lw=l_width, zorder=1)
125
126        elif marker == 'step':
127            line = self.ax.step(x, y, color=color, marker='', linestyle='-',
128                                label=self._title, lw=l_width, zorder=1)[0]
129
130        else:
131            # plot data with/without errorbars
132            if hide_error:
133                line = ax.plot(x, y, marker=marker, color=color, markersize=markersize,
134                        linestyle='', label=self._title, picker=True)
135            else:
136                line = ax.errorbar(x, y,
137                            yerr=self._data.view.dy, xerr=None,
138                            capsize=2, linestyle='',
139                            barsabove=False,
140                            color=color,
141                            marker=marker,
142                            markersize=markersize,
143                            lolims=False, uplims=False,
144                            xlolims=False, xuplims=False,
145                            label=self._title,
146                            picker=True)
147
148        # Update the list of data sets (plots) in chart
149        self.plot_dict[self._data.id] = self.data
150
151        # Now add the legend with some customizations.
152
153        self.legend = ax.legend(loc='upper right', shadow=True)
154        if self.legend:
155            self.legend.set_picker(True)
156
157        # Current labels for axes
158        if self.y_label and not is_fit:
159            ax.set_ylabel(self.y_label)
160        if self.x_label and not is_fit:
161            ax.set_xlabel(self.x_label)
162
163        # Include scaling (log vs. linear)
164        ax.set_xscale(self.xscale)
165        ax.set_yscale(self.yscale)
166
167        # define the ranges
168        self.setRange = SetGraphRange(parent=self,
169            x_range=self.ax.get_xlim(), y_range=self.ax.get_ylim())
170
171        # refresh canvas
172        self.canvas.draw_idle()
173
174    def createContextMenu(self):
175        """
176        Define common context menu and associated actions for the MPL widget
177        """
178        self.defaultContextMenu()
179
180        # Separate plots
181        self.addPlotsToContextMenu()
182
183        # Additional menu items
184        self.contextMenu.addSeparator()
185        self.actionAddText = self.contextMenu.addAction("Add Text")
186        self.actionRemoveText = self.contextMenu.addAction("Remove Text")
187        self.contextMenu.addSeparator()
188        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
189        self.contextMenu.addSeparator()
190        self.actionSetGraphRange = self.contextMenu.addAction("Set Graph Range")
191        self.actionResetGraphRange =\
192            self.contextMenu.addAction("Reset Graph Range")
193        # Add the title change for dialogs
194        #if self.parent:
195        self.contextMenu.addSeparator()
196        self.actionWindowTitle = self.contextMenu.addAction("Window Title")
197
198        # Define the callbacks
199        self.actionAddText.triggered.connect(self.onAddText)
200        self.actionRemoveText.triggered.connect(self.onRemoveText)
201        self.actionChangeScale.triggered.connect(self.onScaleChange)
202        self.actionSetGraphRange.triggered.connect(self.onSetGraphRange)
203        self.actionResetGraphRange.triggered.connect(self.onResetGraphRange)
204        self.actionWindowTitle.triggered.connect(self.onWindowsTitle)
205
206    def addPlotsToContextMenu(self):
207        """
208        Adds operations on all plotted sets of data to the context menu
209        """
210        for id in list(self.plot_dict.keys()):
211            plot = self.plot_dict[id]
212
213            name = plot.name if plot.name else plot.title
214            plot_menu = self.contextMenu.addMenu('&%s' % name)
215
216            self.actionDataInfo = plot_menu.addAction("&DataInfo")
217            self.actionDataInfo.triggered.connect(
218                                functools.partial(self.onDataInfo, plot))
219
220            self.actionSavePointsAsFile = plot_menu.addAction("&Save Points as a File")
221            self.actionSavePointsAsFile.triggered.connect(
222                                functools.partial(self.onSavePoints, plot))
223            plot_menu.addSeparator()
224
225            if plot.id != 'fit':
226                self.actionLinearFit = plot_menu.addAction('&Linear Fit')
227                self.actionLinearFit.triggered.connect(
228                                functools.partial(self.onLinearFit, id))
229                plot_menu.addSeparator()
230
231            self.actionRemovePlot = plot_menu.addAction("Remove")
232            self.actionRemovePlot.triggered.connect(
233                                functools.partial(self.onRemovePlot, id))
234
235            if not plot.is_data:
236                self.actionFreeze = plot_menu.addAction('&Freeze')
237                self.actionFreeze.triggered.connect(
238                                functools.partial(self.onFreeze, id))
239            plot_menu.addSeparator()
240
241            if plot.is_data:
242                self.actionHideError = plot_menu.addAction("Hide Error Bar")
243                if plot.dy is not None and plot.dy != []:
244                    if plot.hide_error:
245                        self.actionHideError.setText('Show Error Bar')
246                else:
247                    self.actionHideError.setEnabled(False)
248                self.actionHideError.triggered.connect(
249                                functools.partial(self.onToggleHideError, id))
250                plot_menu.addSeparator()
251
252            self.actionModifyPlot = plot_menu.addAction('&Modify Plot Property')
253            self.actionModifyPlot.triggered.connect(
254                                functools.partial(self.onModifyPlot, id))
255
256    def createContextMenuQuick(self):
257        """
258        Define context menu and associated actions for the quickplot MPL widget
259        """
260        # Default actions
261        self.defaultContextMenu()
262
263        # Additional actions
264        self.actionToggleGrid = self.contextMenu.addAction("Toggle Grid On/Off")
265        self.contextMenu.addSeparator()
266        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
267
268        # Define the callbacks
269        self.actionToggleGrid.triggered.connect(self.onGridToggle)
270        self.actionChangeScale.triggered.connect(self.onScaleChange)
271
272    def onScaleChange(self):
273        """
274        Show a dialog allowing axes rescaling
275        """
276        if self.properties.exec_() == QtWidgets.QDialog.Accepted:
277            self.xLogLabel, self.yLogLabel = self.properties.getValues()
278            self.xyTransform(self.xLogLabel, self.yLogLabel)
279
280    def onAddText(self):
281        """
282        Show a dialog allowing adding custom text to the chart
283        """
284        if self.addText.exec_() != QtWidgets.QDialog.Accepted:
285            return
286
287        # Retrieve the new text, its font and color
288        extra_text = self.addText.text()
289        extra_font = self.addText.font()
290        extra_color = self.addText.color()
291
292        # Place the text on the screen at the click location
293        pos_x = self.x_click
294        pos_y = self.y_click
295
296        # Map QFont onto MPL font
297        mpl_font = FontProperties()
298        mpl_font.set_size(int(extra_font.pointSize()))
299        mpl_font.set_family(str(extra_font.family()))
300        mpl_font.set_weight(int(extra_font.weight()))
301        # MPL style names
302        styles = ['normal', 'italic', 'oblique']
303        # QFont::Style maps directly onto the above
304        try:
305            mpl_font.set_style(styles[extra_font.style()])
306        except:
307            pass
308
309        if len(extra_text) > 0:
310            new_text = self.ax.text(pos_x,
311                                    pos_y,
312                                    extra_text,
313                                    color=extra_color,
314                                    fontproperties=mpl_font)
315
316            # Update the list of annotations
317            self.textList.append(new_text)
318            self.canvas.draw()
319
320    def onRemoveText(self):
321        """
322        Remove the most recently added text
323        """
324        num_text = len(self.textList)
325        if num_text < 1:
326            return
327        txt = self.textList[num_text - 1]
328        text_remove = txt.get_text()
329        try:
330            txt.remove()
331        except ValueError:
332            # Text got already deleted somehow
333            pass
334        self.textList.remove(txt)
335
336        self.canvas.draw_idle()
337
338    def onSetGraphRange(self):
339        """
340        Show a dialog allowing setting the chart ranges
341        """
342        # min and max of data
343        if self.setRange.exec_() == QtWidgets.QDialog.Accepted:
344            x_range = self.setRange.xrange()
345            y_range = self.setRange.yrange()
346            if x_range is not None and y_range is not None:
347                self.ax.set_xlim(x_range)
348                self.ax.set_ylim(y_range)
349                self.canvas.draw_idle()
350
351    def onResetGraphRange(self):
352        """
353        Resets the chart X and Y ranges to their original values
354        """
355        x_range = (self.data.x.min(), self.data.x.max())
356        y_range = (self.data.y.min(), self.data.y.max())
357        if x_range is not None and y_range is not None:
358            self.ax.set_xlim(x_range)
359            self.ax.set_ylim(y_range)
360            self.canvas.draw_idle()
361
362    def onLinearFit(self, id):
363        """
364        Creates and displays a simple linear fit for the selected plot
365        """
366        selected_plot = self.plot_dict[id]
367
368        maxrange = (min(selected_plot.x), max(selected_plot.x))
369        fitrange = self.ax.get_xlim()
370
371        fit_dialog = LinearFit(parent=self,
372                    data=selected_plot,
373                    max_range=maxrange,
374                    fit_range=fitrange,
375                    xlabel=self.xLogLabel,
376                    ylabel=self.yLogLabel)
377        fit_dialog.updatePlot.connect(self.onFitDisplay)
378        if fit_dialog.exec_() == QtWidgets.QDialog.Accepted:
379            return
380
381    def replacePlot(self, id, new_plot):
382        """
383        Remove plot 'id' and add 'new_plot' to the chart.
384        This effectlvely refreshes the chart with changes to one of its plots
385        """
386        self.removePlot(id)
387        self.plot(data=new_plot)
388
389    def onRemovePlot(self, id):
390        """
391        Responds to the plot delete action
392        """
393        self.removePlot(id)
394
395        if len(self.plot_dict) == 0:
396            # last plot: graph is empty must be the panel must be destroyed
397                self.parent.close()
398
399    def removePlot(self, id):
400        """
401        Deletes the selected plot from the chart
402        """
403        if id not in list(self.plot_dict.keys()):
404            return
405
406        selected_plot = self.plot_dict[id]
407
408        plot_dict = copy.deepcopy(self.plot_dict)
409
410        # Labels might have been changed
411        xl = self.ax.xaxis.label.get_text()
412        yl = self.ax.yaxis.label.get_text()
413
414        self.plot_dict = {}
415
416        plt.cla()
417        self.ax.cla()
418
419        for ids in plot_dict:
420            if ids != id:
421                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)
422
423        # Reset the labels
424        self.ax.set_xlabel(xl)
425        self.ax.set_ylabel(yl)
426        self.canvas.draw()
427
428    def onFreeze(self, id):
429        """
430        Freezes the selected plot to a separate chart
431        """
432        plot = self.plot_dict[id]
433        self.manager.add_data(data_list=[plot])
434
435    def onModifyPlot(self, id):
436        """
437        Allows for MPL modifications to the selected plot
438        """
439        selected_plot = self.plot_dict[id]
440
441        # Old style color - single integer for enum color
442        # New style color - #hhhhhh
443        color = selected_plot.custom_color
444        # marker symbol and size
445        marker = selected_plot.symbol
446        marker_size = selected_plot.markersize
447        # plot name
448        legend = selected_plot.title
449
450        plotPropertiesWidget = PlotProperties(self,
451                                color=color,
452                                marker=marker,
453                                marker_size=marker_size,
454                                legend=legend)
455        if plotPropertiesWidget.exec_() == QtWidgets.QDialog.Accepted:
456            # Update Data1d
457            selected_plot.markersize = plotPropertiesWidget.markersize()
458            selected_plot.custom_color = plotPropertiesWidget.color()
459            selected_plot.symbol = plotPropertiesWidget.marker()
460            selected_plot.title = plotPropertiesWidget.legend()
461
462            # Redraw the plot
463            self.replacePlot(id, selected_plot)
464
465    def onToggleHideError(self, id):
466        """
467        Toggles hide error/show error menu item
468        """
469        selected_plot = self.plot_dict[id]
470        current = selected_plot.hide_error
471
472        # Flip the flag
473        selected_plot.hide_error = not current
474
475        plot_dict = copy.deepcopy(self.plot_dict)
476        self.plot_dict = {}
477
478        # Clean the canvas
479        plt.cla()
480        self.ax.cla()
481
482        # Recreate the plots but reverse the error flag for the current
483        for ids in plot_dict:
484            if ids == id:
485                self.plot(data=plot_dict[ids], hide_error=(not current))
486            else:
487                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)               
488
489    def xyTransform(self, xLabel="", yLabel=""):
490        """
491        Transforms x and y in View and set the scale
492        """
493        # Transform all the plots on the chart
494        for id in list(self.plot_dict.keys()):
495            current_plot = self.plot_dict[id]
496            if current_plot.id == "fit":
497                self.removePlot(id)
498                continue
499
500            new_xlabel, new_ylabel, xscale, yscale =\
501                GuiUtils.xyTransform(current_plot, xLabel, yLabel)
502            self.xscale = xscale
503            self.yscale = yscale
504
505            # Plot the updated chart
506            self.removePlot(id)
507
508            # This assignment will wrap the label in Latex "$"
509            self.xLabel = new_xlabel
510            self.yLabel = new_ylabel
511            # Directly overwrite the data to avoid label reassignment
512            self._data = current_plot
513            self.plot()
514
515        pass # debug hook
516
517    def onFitDisplay(self, fit_data):
518        """
519        Add a linear fitting line to the chart
520        """
521        # Create new data structure with fitting result
522        tempx = fit_data[0]
523        tempy = fit_data[1]
524        self.fit_result.x = []
525        self.fit_result.y = []
526        self.fit_result.x = tempx
527        self.fit_result.y = tempy
528        self.fit_result.dx = None
529        self.fit_result.dy = None
530
531        #Remove another Fit, if exists
532        self.removePlot("fit")
533
534        self.fit_result.reset_view()
535        #self.offset_graph()
536
537        # Set plot properties
538        self.fit_result.id = 'fit'
539        self.fit_result.title = 'Fit'
540        self.fit_result.name = 'Fit'
541
542        # Plot the line
543        self.plot(data=self.fit_result, marker='-', hide_error=True)
544
545    def onMplMouseDown(self, event):
546        """
547        Left button down and ready to drag
548        """
549        # Check that the LEFT button was pressed
550        if event.button != 1:
551            return
552
553        self.leftdown = True
554        for text in self.textList:
555            if text.contains(event)[0]: # If user has clicked on text
556                self.selectedText = text
557                return
558        if event.inaxes is None:
559            return
560        try:
561            self.x_click = float(event.xdata)  # / size_x
562            self.y_click = float(event.ydata)  # / size_y
563        except:
564            self.position = None
565
566    def onMplMouseUp(self, event):
567        """
568        Set the data coordinates of the click
569        """
570        self.x_click = event.xdata
571        self.y_click = event.ydata
572
573        # Check that the LEFT button was released
574        if event.button == 1:
575            self.leftdown = False
576            self.selectedText = None
577
578        #release the legend
579        if self.gotLegend == 1:
580            self.gotLegend = 0
581
582    def onMplMouseMotion(self, event):
583        """
584        Check if the left button is press and the mouse in moving.
585        Compute delta for x and y coordinates and then perform the drag
586        """
587        if self.gotLegend == 1 and self.leftdown:
588            self.onLegendMotion(event)
589            return
590
591        #if self.leftdown and self.selectedText is not None:
592        if not self.leftdown or self.selectedText is None:
593            return
594        # User has clicked on text and is dragging
595        if event.inaxes is None:
596            # User has dragged outside of axes
597            self.selectedText = None
598        else:
599            # Only move text if mouse is within axes
600            self.selectedText.set_position((event.xdata, event.ydata))
601            self.canvas.draw_idle()
602        return
603
604    def onMplPick(self, event):
605        """
606        On pick legend
607        """
608        legend = self.legend
609        if event.artist != legend:
610            return
611        # Get the box of the legend.
612        bbox = self.legend.get_window_extent()
613        # Get mouse coordinates at time of pick.
614        self.mouse_x = event.mouseevent.x
615        self.mouse_y = event.mouseevent.y
616        # Get legend coordinates at time of pick.
617        self.legend_x = bbox.xmin
618        self.legend_y = bbox.ymin
619        # Indicate we picked up the legend.
620        self.gotLegend = 1
621
622        #self.legend.legendPatch.set_alpha(0.5)
623
624    def onLegendMotion(self, event):
625        """
626        On legend in motion
627        """
628        ax = event.inaxes
629        if ax is None:
630            return
631        # Event occurred inside a plotting area
632        lo_x, hi_x = ax.get_xlim()
633        lo_y, hi_y = ax.get_ylim()
634        # How much the mouse moved.
635        x = mouse_diff_x = self.mouse_x - event.x
636        y = mouse_diff_y = self.mouse_y - event.y
637        # Put back inside
638        if x < lo_x:
639            x = lo_x
640        if x > hi_x:
641            x = hi_x
642        if y < lo_y:
643            y = lo_y
644        if y > hi_y:
645            y = hi_y
646        # Move the legend from its previous location by that same amount
647        loc_in_canvas = self.legend_x - mouse_diff_x, \
648                        self.legend_y - mouse_diff_y
649        # Transform into legend coordinate system
650        trans_axes = self.legend.parent.transAxes.inverted()
651        loc_in_norm_axes = trans_axes.transform_point(loc_in_canvas)
652        self.legend_pos_loc = tuple(loc_in_norm_axes)
653        self.legend._loc = self.legend_pos_loc
654        # self.canvas.draw()
655        self.canvas.draw_idle()
656
657    def onMplWheel(self, event):
658        """
659        Process mouse wheel as zoom events
660        """
661        ax = event.inaxes
662        step = event.step
663
664        if ax is not None:
665            # Event occurred inside a plotting area
666            lo, hi = ax.get_xlim()
667            lo, hi = PlotUtilities.rescale(lo, hi, step,
668                              pt=event.xdata, scale=ax.get_xscale())
669            if not self.xscale == 'log' or lo > 0:
670                self._scale_xlo = lo
671                self._scale_xhi = hi
672                ax.set_xlim((lo, hi))
673
674            lo, hi = ax.get_ylim()
675            lo, hi = PlotUtilities.rescale(lo, hi, step, pt=event.ydata,
676                              scale=ax.get_yscale())
677            if not self.yscale == 'log' or lo > 0:
678                self._scale_ylo = lo
679                self._scale_yhi = hi
680                ax.set_ylim((lo, hi))
681        else:
682            # Check if zoom happens in the axes
683            xdata, ydata = None, None
684            x, y = event.x, event.y
685
686            for ax in self.axes:
687                insidex, _ = ax.xaxis.contains(event)
688                if insidex:
689                    xdata, _ = ax.transAxes.inverted().transform_point((x, y))
690                insidey, _ = ax.yaxis.contains(event)
691                if insidey:
692                    _, ydata = ax.transAxes.inverted().transform_point((x, y))
693            if xdata is not None:
694                lo, hi = ax.get_xlim()
695                lo, hi = PlotUtilities.rescale(lo, hi, step,
696                                  bal=xdata, scale=ax.get_xscale())
697                if not self.xscale == 'log' or lo > 0:
698                    self._scale_xlo = lo
699                    self._scale_xhi = hi
700                    ax.set_xlim((lo, hi))
701            if ydata is not None:
702                lo, hi = ax.get_ylim()
703                lo, hi = PlotUtilities.rescale(lo, hi, step, bal=ydata,
704                                  scale=ax.get_yscale())
705                if not self.yscale == 'log' or lo > 0:
706                    self._scale_ylo = lo
707                    self._scale_yhi = hi
708                    ax.set_ylim((lo, hi))
709        self.canvas.draw_idle()
710
711
712class Plotter(QtWidgets.QDialog, PlotterWidget):
713    def __init__(self, parent=None, quickplot=False):
714
715        QtWidgets.QDialog.__init__(self)
716        PlotterWidget.__init__(self, parent=self, manager=parent, quickplot=quickplot)
717        icon = QtGui.QIcon()
718        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
719        self.setWindowIcon(icon)
720
721
Note: See TracBrowser for help on using the repository browser.