1# -*- coding: utf-8 -*-
2
3#-------------------------------------------------------------------------------
4
5# This file is part of Code_Saturne, a general-purpose CFD tool.
6#
7# Copyright (C) 1998-2021 EDF S.A.
8#
9# This program is free software; you can redistribute it and/or modify it under
10# the terms of the GNU General Public License as published by the Free Software
11# Foundation; either version 2 of the License, or (at your option) any later
12# version.
13#
14# This program is distributed in the hope that it will be useful, but WITHOUT
15# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17# details.
18#
19# You should have received a copy of the GNU General Public License along with
20# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
21# Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
23#-------------------------------------------------------------------------------
24
25"""
26This module defines basic classes used for the Pages construction.
27
28This module defines the following classes:
29- ComboModel
30- IntValidator
31- DoubleValidator
32- RegExpValidator
33"""
34
35#-------------------------------------------------------------------------------
36# Library modules import
37#-------------------------------------------------------------------------------
38
39import sys
40import os
41import logging
42import locale
43
44Py2 = sys.version[0] == '2'
45Py3 = sys.version[0] == '3'
46
47
48#-------------------------------------------------------------------------------
49# Third-party modules
50#-------------------------------------------------------------------------------
51
52from code_saturne.Base.QtCore    import *
53from code_saturne.Base.QtGui     import *
54from code_saturne.Base.QtWidgets import *
55
56#-------------------------------------------------------------------------------
57# Application modules import
58#-------------------------------------------------------------------------------
59
60from code_saturne.model.Common import LABEL_LENGTH_MAX, GuiParam
61
62#-------------------------------------------------------------------------------
63# log config
64#-------------------------------------------------------------------------------
65
66logging.basicConfig()
67log = logging.getLogger("QtPage")
68log.setLevel(GuiParam.DEBUG)
69
70#-------------------------------------------------------------------------------
71# Compatibility PyQt API #1 and #2 and PySide
72#-------------------------------------------------------------------------------
73
74#==============================================================================
75# Data types
76#==============================================================================
77if Py2:
78    TEXT_TYPES = (str, unicode)
79else:
80    TEXT_TYPES = (str,)
81
82#==============================================================================
83# Classes used to handle ComboBox subsections (groups)
84#==============================================================================
85
86# Qt Role used to differentiate whether a ComboBox entry is a group child
87# or not
88GroupRole = Qt.UserRole
89
90
91class GroupDelegate(QStyledItemDelegate):
92    """
93    Class used to create a group delegate which applies a tab to a combobox
94    entry if it is a group child. Only applied to the visual style!
95    """
96    def initStyleOption(self, option, index):
97        super(GroupDelegate, self).initStyleOption(option, index)
98        if not index.data(GroupRole):
99            option.text = "   " + option.text
100
101class GroupItem():
102    """
103    GroupItem class.
104    Handles the subsections in a ComboModel.
105    """
106
107    def __init__(self, name, index):
108        """
109        Init method. Inputs are:
110        name  : String with the name of the group
111        index : Position of the group name in the combobox list of elements
112        """
113        self._name  = name
114        self._index = index
115
116        self._children = []
117        self._number_of_children = 0
118
119
120    def addChild(self, name):
121        """
122        Method to add a child with the given name.
123        Returns the index of the child to be used if the 'insertRow' method.
124        """
125        self._children.append(name)
126        self._number_of_children += 1
127
128        child_index = self._index + self._number_of_children
129
130        return child_index
131
132    def getIndex(self):
133        """
134        Method which returns the index of the group
135        """
136        return self._index
137
138    def getChildIndex(self, childName):
139        """
140        Method to retrieve the index of a given child
141        """
142        if childName not in self._children:
143            msg = "%s is not part of group %s\n" % (childName, self._name)
144            raise Exception(msg)
145
146        child_index = self._children.index(childName) + self._index + 1
147        return child_index
148
149
150    def getNumberOfChildren(self):
151        """
152        Method which returns the number of children in the group
153        """
154        return self._number_of_children
155
156
157    def incrementIndex(self):
158
159        self._index += 1
160
161    def generateItem(self):
162
163        item  = QStandardItem(str(self._name))
164        item.setData(True, GroupRole)
165
166        font = item.font()
167        font.setBold(True)
168        font.setItalic(True)
169        item.setFont(font)
170        item.setFlags(item.flags() & ~Qt.ItemIsSelectable)
171
172        return item
173#==============================================================================
174# Strings
175#==============================================================================
176
177def is_text_string(obj):
178    """Return True if `obj` is a text string,
179              False if it is anything else,
180                    like binary data (Python 3) or
181                    QString (Python 2, PyQt API #1)"""
182    if Py2:
183        return isinstance(obj, basestring)
184    else:
185        return isinstance(obj, str)
186
187def to_text_string(obj, encoding=None):
188    """Convert `obj` to (unicode) text string"""
189    if Py2:
190        if encoding is None:
191            return unicode(obj)
192        else:
193            return unicode(obj, encoding)
194    else:
195        if encoding is None:
196            return str(obj)
197        elif isinstance(obj, str):
198            # In case this function is not used properly, this could happen
199            return obj
200        else:
201            return str(obj, encoding)
202
203#==============================================================================
204# QVariant conversion utilities
205#
206# Note: to_qvariant was removed recently; from_qvariant may be removed in
207#       many_places and the "to_text_string" variants could be replaced
208#       by "to_str"; where the value is already known to be of the correct
209#       type, or None handled in the associated code,
210#       this could even be ignored
211#==============================================================================
212
213import collections
214
215if os.environ.get('QT_API', 'pyqt') == 'pyqt':
216
217    def from_qvariant(qobj=None, convfunc=None):
218        """Convert QVariant object to Python object
219        This is a transitional function from PyQt API #1 (QVariant exists)
220        to PyQt API #2 and Pyside (QVariant does not exist)"""
221        if (qobj != None):
222            if convfunc in TEXT_TYPES or convfunc is to_text_string:
223                return str(qobj)
224            elif convfunc is int:
225                return int(qobj)
226            elif convfunc is float:
227                try:
228                    return float(qobj)
229                except Exception:
230                    return locale.atof(qobj)
231            else:
232                return qobj
233        else:
234            return qobj
235
236else:
237
238    def from_qvariant(qobj=None, pytype=None):
239        """Convert QVariant object to Python object
240        This is a transitional function from PyQt API #1 (QVariant exist)
241        to PyQt API #2 and Pyside (QVariant does not exist)"""
242        return qobj
243
244def qbytearray_to_str(qba):
245    """Convert QByteArray object to str in a way compatible with Python 2/3"""
246    return str(bytes(qba.toHex().data()).decode())
247
248
249def to_str(s):
250    """Convert argument to string, using an empty string for None"""
251    if s is None:
252        return ''
253    return str(s)
254
255#==============================================================================
256# Wrappers around QFileDialog static methods
257#==============================================================================
258
259def getexistingdirectory(parent=None, caption='', basedir='',
260                         options=QFileDialog.ShowDirsOnly):
261    """Wrapper around QtGui.QFileDialog.getExistingDirectory static method
262    Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0"""
263    # Calling QFileDialog static method
264    if sys.platform == "win32":
265        # On Windows platforms: redirect standard outputs
266        _temp1, _temp2 = sys.stdout, sys.stderr
267        sys.stdout, sys.stderr = None, None
268    try:
269        result = QFileDialog.getExistingDirectory(parent, caption, basedir,
270                                                  options)
271    finally:
272        if sys.platform == "win32":
273            # On Windows platforms: restore standard outputs
274            sys.stdout, sys.stderr = _temp1, _temp2
275    if not is_text_string(result):
276        # PyQt API #1
277        result = to_text_string(result)
278    return result
279
280
281def _qfiledialog_wrapper(attr, parent=None, caption='', basedir='',
282                         filters='', selectedfilter='', options=None):
283    if options is None:
284        options = QFileDialog.Options(0)
285
286    try:
287        from code_saturne.Base.QtCore import QString
288    except ImportError:
289        QString = None  # analysis:ignore
290
291    tuple_returned = True
292    try:
293        func = getattr(QFileDialog, attr+'AndFilter')
294    except AttributeError:
295        func = getattr(QFileDialog, attr)
296        if QString is not None:
297            selectedfilter = QString()
298            tuple_returned = False
299
300    if sys.platform == "win32":
301        # On Windows platforms: redirect standard outputs
302        _temp1, _temp2 = sys.stdout, sys.stderr
303        sys.stdout, sys.stderr = None, None
304    try:
305        result = func(parent, caption, basedir,
306                      filters, selectedfilter, options)
307    except TypeError:
308        result = func(parent, caption, basedir, filters, options)
309    finally:
310        if sys.platform == "win32":
311            # On Windows platforms: restore standard outputs
312            sys.stdout, sys.stderr = _temp1, _temp2
313
314    # Processing output
315    if tuple_returned:
316        output, selectedfilter = result
317    else:
318        output = result
319    if QString is not None:
320        # PyQt API #1: conversions needed from QString/QStringList
321        selectedfilter = to_text_string(selectedfilter)
322        if isinstance(output, QString):
323            # Single filename
324            output = to_text_string(output)
325        else:
326            # List of filenames
327            output = [to_text_string(fname) for fname in output]
328
329    # Always returns the tuple (output, selectedfilter)
330    return output, selectedfilter
331
332
333def getopenfilename(parent=None,
334                    caption='',
335                    basedir='',
336                    filters='',
337                    selectedfilter='',
338                    options=None):
339    return _qfiledialog_wrapper('getOpenFileName',
340                                parent=parent,
341                                caption=caption,
342                                basedir=basedir,
343                                filters=filters,
344                                selectedfilter=selectedfilter,
345                                options=options)
346
347
348def getopenfilenames(parent=None,
349                     caption='',
350                     basedir='',
351                     filters='',
352                     selectedfilter='',
353                     options=None):
354    return _qfiledialog_wrapper('getOpenFileNames',
355                                parent=parent,
356                                caption=caption,
357                                basedir=basedir,
358                                filters=filters,
359                                selectedfilter=selectedfilter,
360                                options=options)
361
362
363def getsavefilename(parent=None,
364                    caption='',
365                    basedir='',
366                    filters='',
367                    selectedfilter='',
368                    options=None):
369    return _qfiledialog_wrapper('getSaveFileName',
370                                parent=parent,
371                                caption=caption,
372                                basedir=basedir,
373                                filters=filters,
374                                selectedfilter=selectedfilter,
375                                options=options)
376
377
378#-------------------------------------------------------------------------------
379# QComboBox model
380#-------------------------------------------------------------------------------
381
382class ComboModel:
383    """
384    Class to build a model (QStandardItemModel) used with a QComboBox.
385
386    Main attributes of class are:
387
388    combo: QComboBox passed as arguments. It uses the model
389    model: QStandardItemModel which contains items
390
391    dicoV2M: correspondance between strings in Qt view and strings in parameters
392    dicoM2V: correspondance between strings in parameters and strings in Qt view
393
394    items: tuple which contains all model strings (usefull to get its index in the model)
395    """
396    def __init__(self, combo, rows=0, columns=0):
397        """
398        Initialization
399        """
400        self.combo   = combo
401
402        self.rows    = rows
403        self.columns = columns
404        self.last    = 0
405
406        self.model   = QStandardItemModel()
407        self.model.clear()
408        self.model.setRowCount(rows)
409        self.model.setColumnCount(columns)
410
411        self.dicoV2M = {}
412        self.dicoM2V = {}
413
414        self.item_groups = {}
415
416        self.items   = []
417        self.combo.setModel(self.model)
418        self.combo.setItemDelegate(GroupDelegate(self.combo))
419
420
421    def addItemGroup(self, group_name):
422
423        if group_name in self.item_groups.keys():
424            return
425
426        index = self.last
427        self.item_groups[group_name] = GroupItem(group_name, index)
428
429        item = self.item_groups[group_name].generateItem()
430        self.model.setItem(index, item)
431
432        self.items.append(group_name)
433
434        self.last += 1
435
436    def addItemList(self, list_of_views_and_models, warn=False, groupName=None):
437        """
438        Insert of a list of items in the model.
439        list_of_views_and_models is a list of 2-element list
440          e.g. [ ["View1", "Model1"], ["View2", "Model2"] ]
441        """
442        for view, model in list_of_views_and_models:
443            self.addItem(view, model, warn, groupName)
444
445    def hasItem(self, index=None, str_view="", str_model=""):
446        if index is not None:
447            return index < self.last
448
449        elif str_model:
450            return (str_model in self.items)
451
452        elif str_view:
453            return (str_view in self.dicoV2M.keys())
454
455    def addItem(self, str_view, str_model="", warn=False, groupName=None):
456        """
457        Insert an item in the model.
458
459        str_view: string to be displayed in the view.
460        For example, 'Eulerian/Lagrangian Multi-phase Treatment'
461
462        str_model: correponding string used in the model.
463        For example, 'lagrangian'
464
465        warn: If True, entry is marked with a color.
466        """
467        if self.hasItem(str_view=str_view):
468            return
469
470        item = QStandardItem(str(str_view))
471        item.setData(True, GroupRole)
472
473        if groupName in self.item_groups.keys():
474            # Insert the child after the group name in the ComboModel
475            item.setData(False, GroupRole)
476            index = self.item_groups[groupName].addChild(str(str_view))
477            self.model.setItem(index, item)
478
479            # In case groups were created before, update the index of
480            # the following groups in the list
481            for key in self.item_groups.keys():
482                gpe = self.item_groups[key]
483                if gpe.getIndex() >= index:
484                    gpe.incrementIndex()
485
486                    idx = gpe.getIndex()
487                    it  = gpe.generateItem()
488                    self.model.setItem(idx, it)
489
490        else:
491            index = self.last
492            self.model.setItem(index, item)
493
494        if warn:
495            self.combo.setItemData(index,
496                                   QColor(Qt.red),
497                                   Qt.TextColorRole)
498
499        self.last += 1
500
501        if not str_model: str_model = str_view
502
503        self.items.append(str_model)
504
505        self.dicoM2V[str_model] = str_view
506        self.dicoV2M[str_view]  = str_model
507
508
509    def modifyItem(self, old_str_view, new_str_view, new_str_model=""):
510        """
511        Modify string names.
512        """
513        if old_str_view in self.items:
514            index = self.items.index(old_str_view)
515            self.items[index] = new_str_view
516
517            old_str_model = dicoV2M[old_str_view]
518            if new_str_model == "":
519                new_str_model = old_str_model
520
521            del self.dicoM2V[str_model]
522            del self.dicoV2M[str_view]
523
524            self.dicoM2V[new_str_model] = new_str_view
525            self.dicoV2M[new_str_view] = new_str_model
526
527    def flushItems(self):
528        """
529        Remove all items
530        """
531        while self.last > 0:
532            self.delItem(0)
533
534    def delItem(self, index=None, str_model="", str_view=""):
535        """
536        Remove the item specified with its index or a string.
537        """
538        if index is not None:
539            self.__deleteItem(index)
540
541        elif str_model:
542            index = self.items.index(str_model)
543            self.__deleteItem(index)
544
545        elif str_view:
546            str_model = self.dicoV2M[str_view]
547            index = self.items.index(str_model)
548            self.__deleteItem(index)
549        self.combo.removeItem(index)
550        self.last = self.last - 1
551
552
553    def __deleteItem(self, index):
554        """
555        Delete the item specified with its index
556        """
557        str_model  = self.items[index]
558        str_view = self.dicoM2V[str_model]
559        del self.items[index]
560        del self.dicoV2M[str_view]
561        del self.dicoM2V[str_model]
562
563
564    def __disableItem(self, index):
565        """
566        Disable the item specified with its index
567        """
568        self.model.item(index).setEnabled(False)
569
570
571    def __enableItem(self, index):
572        """
573        Enable the item specified with its index
574        """
575        self.model.item(index).setEnabled(True)
576
577
578    def disableItem(self, index=None, str_model="", str_view=""):
579        """
580        Disable the item specified with its index or a string.
581        """
582        if index is not None:
583            self.__disableItem(index)
584
585        elif str_model:
586            index = self.items.index(str_model)
587            self.__disableItem(index)
588
589        elif str_view:
590            str_model = self.dicoV2M[str_view]
591            index = self.items.index(str_model)
592            self.__disableItem(index)
593
594
595    def enableItem(self, index=None, str_model="", str_view=""):
596        """
597        Enable the item specified with its index or a string.
598        """
599        if index is not None:
600            self.__enableItem(index)
601
602        elif str_model:
603            index = self.items.index(str_model)
604            self.__enableItem(index)
605
606        elif str_view:
607            str_model = self.dicoV2M[str_view]
608            index = self.items.index(str_model)
609            self.__enableItem(index)
610
611
612    def setItem(self, index=None, str_model="", str_view=""):
613        """
614        Set item as current.
615        """
616        if index is not None:
617            self.combo.setCurrentIndex(index)
618
619        elif str_model:
620            try:
621                index = self.items.index(str_model)
622                self.combo.setCurrentIndex(index)
623            except Exception:
624                self._displayWarning(str_model)
625                # Throw signals to ensure XML is updated (not very elegant)
626                self.combo.activated[str].emit(self.dicoM2V[self.items[0]])
627                self.combo.currentTextChanged[str].emit(self.dicoM2V[self.items[0]])
628                self.combo.currentIndexChanged[int].emit(0)
629                index = 0
630
631        elif str_view:
632            try:
633                str_model = self.dicoV2M[str_view]
634                index = self.items.index(str_model)
635                self.combo.setCurrentIndex(index)
636            except Exception:
637                self._displayWarning(str_model)
638                index = 0
639
640    def _displayWarning(self, str_model):
641        if self.combo.accessibleName() != "":
642            name = self.combo.accessibleName()
643        else:
644            name = self.combo.objectName()
645        title = "Warning in " + name
646        msg = str_model + " is not in list: " + str(self.items) + "\n"
647        msg += "Value reset to " + str(self.items[0]) + " but should be checked.\n"
648        QMessageBox.warning(self.combo, title, msg)
649
650    def enableItem(self, index=None, str_model="", str_view=""):
651        """
652        Enable the item specified with its index or a string.
653        """
654        if index is not None:
655            self.__enableItem(index)
656
657        elif str_model:
658            index = self.items.index(str_model)
659            self.__enableItem(index)
660
661        elif str_view:
662            str_model = self.dicoV2M[str_view]
663            index = self.items.index(str_model)
664            self.__enableItem(index)
665
666
667    def hide(self):
668        """
669        Hide combobox.
670        """
671        self.combo.hide()
672
673
674    def show(self):
675        """
676        Show combobox.
677        """
678        self.combo.show()
679
680
681    def getItems(self):
682        """
683        Get the tuple of items.
684        """
685        return self.items
686
687#-------------------------------------------------------------------------------
688# Validators for editors
689#-------------------------------------------------------------------------------
690
691vmax = 2147483647
692vmin = -vmax
693
694class IntValidator(QIntValidator):
695    """
696    Validator for integer data.
697    """
698    def __init__(self, parent, min=vmin, max=vmax):
699        """
700        Initialization for validator
701        """
702        QIntValidator.__init__(self, parent)
703        self.parent = parent
704        self.state = QValidator.Invalid
705        self.__min = min
706        self.__max = max
707
708        if type(min) != int or type(max) != int:
709            raise ValueError("The given parameters are not integers (warning: long are not allowed).")
710        self.setBottom(min)
711        self.setTop(max)
712
713        self.exclusiveMin = False
714        self.exclusiveMax = False
715        self.exclusiveValues = []
716
717        self.default = 0
718        self.fix = False
719
720        msg = ""
721        if min > vmin and max == vmax:
722            msg = self.tr("The integer value must be greater than or equal to %i" % min)
723        elif min == vmin and max < vmax:
724            msg = self.tr("The integer value must be lower than or equal to %i" % max)
725        elif min > vmin and max < vmax:
726            msg = self.tr("The integer value must be between %i and %i" % (min, max))
727
728        self.parent.setStatusTip(msg)
729
730
731    def setExclusiveMin(self, b=True):
732        if type(b) != bool:
733            raise ValueError("The given parameter is not a boolean.")
734        self.exclusiveMin = b
735
736        msg = ""
737        if self.__min > vmin and self.__max == vmax:
738            msg = self.tr("The integer value must be greater than %i" % self.__min)
739        elif self.__min == vmin and self.__max < vmax:
740            msg = self.tr("The integer value must be lower than or equal to %i" % self.__max)
741        elif self.__min > vmin and self.__max < vmax:
742            msg = self.tr("The integer value must be greater %i and lower than or equal to %i" % (self.__min, self.__max))
743
744        self.parent.setStatusTip(msg)
745
746
747    def setExclusiveMax(self, b=True):
748        if type(b) != bool:
749            raise ValueError("The given parameter is not a boolean.")
750        self.exclusiveMax = b
751
752        msg = ""
753        if self.__min > vmin and self.__max == vmax:
754            msg = self.tr("The integer value must be greater than or equal to %i" % self.__min)
755        elif self.__min == vmin and self.__max < vmax:
756            msg = self.tr("The integer value must be lower than %i" % self.__max)
757        elif self.__min > vmin and self.__max < vmax:
758            msg = self.tr("The integer value must be greater than or equal to %i and lower than %i" % (self.__min, self.__max))
759
760        self.parent.setStatusTip(msg)
761
762
763    def setExclusiveValues(self, l):
764        if type(l) != list and type(l) != tuple:
765            raise ValueError("The given parameter is not a list or a tuple.")
766        self.exclusiveValues = l
767
768        msg = ""
769        for v in l:
770            if self.__min > vmin or self.__max < vmax:
771                msg = self.tr("All integers value must be greater than %i and lower than %i" % (self.__min, self.__max))
772
773        self.parent.setStatusTip(msg)
774
775
776    def setFixup(self, v):
777        if type(v) != int:
778            raise ValueError("The given parameter is not an integer.")
779        self.default = v
780        self.fix = True
781
782
783    def fixup(self, stri):
784        if self.fix:
785            if not stri:
786                stri = str(self.default)
787
788
789    def validate(self, stri, pos):
790        """
791        Validation method.
792
793        QValidator.Invalid       0  The string is clearly invalid.
794        QValidator.Intermediate  1  The string is a plausible intermediate value during editing.
795        QValidator.Acceptable    2  The string is acceptable as a final result; i.e. it is valid.
796        """
797        state = QIntValidator.validate(self, stri, pos)[0]
798
799        try:
800            x = from_qvariant(stri, int)
801            valid = True
802            pass
803        except (TypeError, ValueError):
804            x = 0
805            valid = False
806
807        if state == QValidator.Acceptable:
808            if self.exclusiveMin and x == self.bottom():
809                state = QValidator.Intermediate
810            elif self.exclusiveMax and x == self.top():
811                state = QValidator.Intermediate
812            elif x in self.exclusiveValues:
813                state = QValidator.Intermediate
814
815        palette = self.parent.palette()
816
817        if not valid or state == QValidator.Intermediate:
818            palette.setColor(QPalette.Text, QColor("red"))
819            self.parent.setPalette(palette)
820        else:
821            palette.setColor(QPalette.Text, QColor("black"))
822            self.parent.setPalette(palette)
823
824        self.state = state
825
826        return (state, stri, pos)
827
828
829    def tr(self, text):
830        """
831        """
832        return text
833
834
835class DoubleValidator(QDoubleValidator):
836    """
837    Validator for real data.
838    """
839    def __init__(self, parent, min=-1.e99, max=1.e99):
840        """
841        Initialization for validator
842        """
843        QDoubleValidator.__init__(self, parent)
844        self.setLocale(QLocale(QLocale.C, QLocale.AnyCountry))
845        self.parent = parent
846        self.state = QValidator.Invalid
847        self.__min = min
848        self.__max = max
849
850        self.setNotation(self.ScientificNotation)
851
852        if type(min) != float or type(max) != float:
853            raise ValueError("The given parameters are not floats.")
854        self.setBottom(min)
855        self.setTop(max)
856
857        self.exclusiveMin = False
858        self.exclusiveMax = False
859
860        self.default = 0.0
861        self.fix = False
862
863        msg = ""
864        if min > -1.e99 and max == 1.e99:
865            msg = self.tr("The float value must be greater than %.1f" % min)
866        elif min == -1.e99 and max < 1.e99:
867            msg = self.tr("The float value must be lower than %.1f" % max)
868        elif min > -1.e99 and max < 1.e99:
869            msg = self.tr("The float value must be between than %.1f and %.1f" % (min, max))
870
871        self.parent.setStatusTip(str(msg))
872
873
874    def setExclusiveMin(self, b=True):
875        if type(b) != bool:
876            raise ValueError("The given parameter is not a boolean.")
877        self.exclusiveMin = b
878
879        msg = ""
880        if self.__min > -1.e99 and self.__max == 1.e99:
881            msg = self.tr("The float value must be greater than %.1f" % self.__min)
882        elif self.__min == -1.e99 and self.__max < 1.e99:
883            msg = self.tr("The float value must be lower than or equal to %.1f" % self.__max)
884        elif self.__min > -1.e99 and self.__max < 1.e99:
885            msg = self.tr("The float value must be greater than %.1f and lower than or equal to %.1f" % (self.__min, self.__max))
886
887        self.parent.setStatusTip(str(msg))
888
889
890    def setExclusiveMax(self, b=True):
891        if type(b) != bool:
892            raise ValueError("The given parameter is not a boolean.")
893        self.exclusiveMax = b
894
895        msg = ""
896        if self.__min > -1.e99 and self.__max == 1.e99:
897            msg = self.tr("The float value must be greater than or equal to %.1f" % self.__min)
898        elif self.__min == -1.e99 and self.__max < 1.e99:
899            msg = self.tr("The float value must be lower than %.1f" % self.__max)
900        elif self.__min > -1.e99 and self.__max < 1.e99:
901            msg = self.tr("The float value must be greater than or equal to %.1f and lower than %.1f" % (self.__min, self.__max))
902
903        self.parent.setStatusTip(str(msg))
904
905
906    def setFixup(self, v):
907        if type(v) != float:
908            raise ValueError("The given parameter is not a float.")
909        self.default = v
910        self.fix = True
911
912
913    def fixup(self, stri):
914        if self.fix:
915            if not stri:
916                stri = str(self.default)
917
918
919    def validate(self, stri, pos):
920        """
921        Validation method.
922
923        QValidator.Invalid       0  The string is clearly invalid.
924        QValidator.Intermediate  1  The string is a plausible intermediate value during editing.
925        QValidator.Acceptable    2  The string is acceptable as a final result; i.e. it is valid.
926        """
927        state = QDoubleValidator.validate(self, stri, pos)[0]
928
929        if state == QValidator.Acceptable:
930            try:
931                x = from_qvariant(stri, float)
932            except Exception: # may be type error or localization issue
933                x = 0.0
934                state = QValidator.Intermediate
935
936        if state == QValidator.Acceptable:
937            if self.exclusiveMin and x == self.bottom():
938                state = QValidator.Intermediate
939            elif self.exclusiveMax and x == self.top():
940                state = QValidator.Intermediate
941
942        palette = self.parent.palette()
943
944        if state != QValidator.Acceptable:
945            palette.setColor(QPalette.Text, QColor("red"))
946            self.parent.setPalette(palette)
947        else:
948            palette.setColor(QPalette.Text, QColor("black"))
949            self.parent.setPalette(palette)
950
951        self.state = state
952
953        return (state, stri, pos)
954
955
956    def tr(self, text):
957        """
958        """
959        return text
960
961
962class RegExpValidator(QRegExpValidator):
963    """
964    Validator for regular expression.
965    """
966    def __init__(self, parent, rx, forbidden_labels=None):
967        """
968        Initialization for validator
969        """
970        QRegExpValidator.__init__(self, parent)
971        self.parent = parent
972        self.state = QRegExpValidator.Invalid
973        self.forbidden = forbidden_labels
974
975        self.__validator = QRegExpValidator(rx, parent)
976
977        if "{1," + str(LABEL_LENGTH_MAX) + "}" in rx.pattern():
978            msg = self.tr("The maximum length of the label is %i characters" % LABEL_LENGTH_MAX)
979            self.parent.setStatusTip(str(msg))
980
981
982    def validate(self, stri, pos):
983        """
984        Validation method.
985
986        QValidator.Invalid       0  The string is clearly invalid.
987        QValidator.Intermediate  1  The string is a plausible intermediate value during editing.
988        QValidator.Acceptable    2  The string is acceptable as a final result; i.e. it is valid.
989        """
990        state = self.__validator.validate(stri, pos)[0]
991
992        if self.forbidden:
993            if stri in self.forbidden:
994                state = QValidator.Intermediate
995
996        palette = self.parent.palette()
997
998        if state == QValidator.Intermediate:
999            palette.setColor(QPalette.Text, QColor("red"))
1000            self.parent.setPalette(palette)
1001        else:
1002            palette.setColor(QPalette.Text, QColor("black"))
1003            self.parent.setPalette(palette)
1004
1005        self.state = state
1006
1007        return (state, stri, pos)
1008
1009
1010    def tr(self, text):
1011        """
1012        """
1013        return text
1014
1015#-------------------------------------------------------------------------------
1016# SpinBox progressing by multiplication and division
1017#-------------------------------------------------------------------------------
1018
1019class RankSpinBoxWidget(QSpinBox):
1020    """
1021    Special Spin box for rank stepping.
1022    """
1023    def __init__(self, parent):
1024        """
1025        Constructor
1026        """
1027        QSpinBox.__init__(self, parent)
1028
1029    def stepBy(self, steps):
1030        v = self.value()
1031        if steps > 0:
1032            self.setValue(v*2)
1033        elif steps < 0 and v > 1:
1034            self.setValue(v/2)
1035
1036    def stepEnabled(self):
1037        v = self.value()
1038        if v < 2:
1039            return QAbstractSpinBox.StepUpEnabled
1040        else:
1041            return QAbstractSpinBox.StepUpEnabled | QAbstractSpinBox.StepDownEnabled
1042
1043#-------------------------------------------------------------------------------
1044# SpinBox progressing by multiplication and division for Buffer size
1045#-------------------------------------------------------------------------------
1046
1047class BufferSpinBoxWidget(QSpinBox):
1048    """
1049    Special Spin box for buffer size.
1050    """
1051    def __init__(self, parent):
1052        """
1053        Constructor
1054        """
1055        QSpinBox.__init__(self, parent)
1056        self.basesize = 1024*1024
1057
1058    def stepBy(self, steps):
1059        v = self.value()
1060        if steps > 0:
1061            if v > 0:
1062                self.setValue(v*2)
1063            else:
1064                self.setValue(self.basesize)
1065        elif steps < 0 and v > 0:
1066            self.setValue(v/2)
1067
1068    def textFromValue(self, v):
1069        """
1070        Define text to be shown.
1071        This text uses a local suffix (not that of the QSpinBox),
1072        as the suffix and value shown are dynamically related.
1073        """
1074        tv = v
1075        suffix = ''
1076        if v >= 1073741824 and v % 1073741824 == 0:
1077            tv = v / 1073741824
1078            suffix = ' GiB'
1079        elif v >= 1048576 and v % 1048576 == 0:
1080            tv = v / 1048576
1081            suffix = ' MiB'
1082        elif v >= 1024 and v % 1024 == 0:
1083            tv = v / 1024
1084            suffix = ' KiB'
1085        elif v > 0:
1086            tv = v
1087            suffix = ' B'
1088        else:
1089            tv = 0
1090            suffix = ''
1091        return QSpinBox.textFromValue(self, tv) + suffix
1092
1093    def stepEnabled(self):
1094        v = self.value()
1095        if v < 1:
1096            return QAbstractSpinBox.StepUpEnabled
1097        else:
1098            return QAbstractSpinBox.StepUpEnabled | QAbstractSpinBox.StepDownEnabled
1099
1100#-------------------------------------------------------------------------------
1101# Delegates to use
1102#-------------------------------------------------------------------------------
1103
1104class LabelDelegate(QItemDelegate):
1105    """
1106    Delegate for lines
1107    """
1108
1109    def __init__(self, parent=None, xml_model=None,
1110                 forbidden_labels=None,
1111                 accepted_regex=None,
1112                 auto_completion=[]):
1113        super(LabelDelegate, self).__init__(parent)
1114
1115        self.parent = parent
1116        self.mdl    = xml_model
1117
1118        self._forbidden_labels = forbidden_labels
1119
1120        # Regex
1121        rx = accepted_regex
1122        if rx == None:
1123            rx = "[ -~]*"
1124#            "[\-_A-Za-z0-9]{1," + str(LABEL_LENGTH_MAX) + "}"
1125        self.regExp = QRegExp(rx)
1126
1127        # Auto completion
1128        self._comp_list = auto_completion
1129
1130    def createEditor(self, parent, option, index):
1131
1132        editor = QLineEdit(parent)
1133
1134        v = RegExpValidator(editor, self.regExp, self._forbidden_labels)
1135        editor.setValidator(v)
1136
1137        # Autocompletion if necessary:
1138        if len(self._comp_list) > 0:
1139            completer = QCompleter()
1140            editor.setCompleter(completer)
1141            mc = QStringListModel()
1142            completer.setModel(mc)
1143            mc.setStringList(self._comp_list)
1144
1145        # return editor
1146        return editor
1147
1148    def setEditorData(self, editor, index):
1149
1150        editor.setAutoFillBackground(True)
1151
1152        v = from_qvariant(index.model().data(index, Qt.DisplayRole),
1153                          to_text_string)
1154        self.p_value = str(v)
1155
1156        editor.setText(v)
1157
1158    def setModelData(self, editor, model, index):
1159
1160        if not editor.isModified():
1161            return
1162
1163        if editor.validator().state == QValidator.Acceptable:
1164            p_value = str(editor.text())
1165            model.setData(index, p_value, Qt.DisplayRole)
1166
1167class FloatDelegate(QItemDelegate):
1168
1169    def __init__(self, parent=None, xml_model=None,
1170                 minVal=-1.e99, maxVal=+1.e99):
1171
1172        super(FloatDelegate, self).__init__(parent)
1173
1174        self.parent = parent
1175        self.mdl    = xml_model
1176
1177        self._min   = minVal
1178        if type(self._min) != float:
1179            self._min = -1.e99
1180
1181        self._max   = maxVal
1182        if type(self._max) != float:
1183            self._max = +1.e99
1184
1185    def createEditor(self, parent, option, index):
1186
1187        editor = QLineEdit(parent)
1188
1189        validator = DoubleValidator(editor,
1190                                    min=self._min,
1191                                    max=self._max)
1192
1193        editor.setValidator(validator)
1194
1195        return editor
1196
1197    def setEditorData(self, editor, index):
1198
1199        editor.setAutoFillBackground(True)
1200
1201        value = from_qvariant(index.model().data(index, Qt.DisplayRole),
1202                              to_text_string)
1203        editor.setText(value)
1204
1205    def setModelData(self, editor, model, index):
1206
1207        if editor.validator().state == QValidator.Acceptable:
1208            value = from_qvariant(editor.text(), float)
1209            selectionModel = self.parent.selectionModel()
1210
1211            for idx in selectionModel.selectedIndexes():
1212                if idx.column() == index.column():
1213                    model.setData(idx, value, Qt.DisplayRole)
1214
1215class IntegerDelegate(QItemDelegate):
1216
1217    def __init__(self, parent=None, xml_model=None,
1218                 minVal=-10000000000, maxVal=+10000000000):
1219
1220        super(IntegerDelegate, self).__init__(parent)
1221
1222        self.parent  = parent
1223        self.mdl     = xml_model
1224
1225        self._min = minVal
1226        if type(self._min) != int:
1227            self._min = -10000000000
1228        self._max = maxVal
1229        if type(self._max) != int:
1230            self._max = +10000000000
1231
1232
1233    def createEditor(self, parent, option, index):
1234
1235        editor = QLineEdit(parent)
1236
1237        validator = IntValidator(editor,
1238                                 min=self._min,
1239                                 max=self._max)
1240
1241        editor.setValidator(validator)
1242
1243        return editor
1244
1245    def setEditorData(self, editor, index):
1246
1247        editor.setAutoFillBackground(True)
1248
1249        value = from_qvariant(index.model().data(index, Qt.DisplayRole),
1250                              to_text_string)
1251        editor.setText(value)
1252
1253    def setModelData(self, editor, model, index):
1254
1255        value = from_qvariant(editor.text(), int)
1256
1257        if editor.validator().state == QValidator.Acceptable:
1258            selectionModel = self.parent.selectionModel()
1259            for idx in selectionModel.selectedIndexes():
1260                if idx.column() == index.column():
1261                    model.setData(idx, value, Qt.DisplayRole)
1262
1263
1264class ComboDelegate(QItemDelegate):
1265
1266    def __init__(self, parent=None, xml_model=None,
1267                 opts_list=[], opts_state=None):
1268
1269        super(ComboDelegate, self).__init__(parent)
1270
1271        self.parent       = parent
1272        self.mdl          = xml_model
1273
1274        if opts_state:
1275            if type(opts_state) != list:
1276                raise Exception("Wrong type for opts_state")
1277            if len(opts_state) != len(opts_list):
1278                raise Exception("Wrong length of opts_state")
1279        else:
1280            # To simplify the code which follows, we ensure that
1281            # opts_state is a list with the correct length
1282            opts_state = ["on"]*len(opts_list)
1283
1284        self.opts_list = []
1285        for i, opt in enumerate(opts_list):
1286
1287            if opts_state[i] not in ["on", "na", "off"]:
1288                msg="Wrong state for opts %s : %s" % (opt,opts_state[i])
1289                raise Exception(msg)
1290
1291            ac = True
1292            av = True
1293            if opts_state[i] == "na":
1294                av = False
1295            elif opts_state[i] == "off":
1296                ac = False
1297                av = False
1298
1299            self.opts_list.append({'name':opt,
1300                                   'active':ac,
1301                                   'available':av})
1302
1303
1304    def createEditor(self, parent, option, index):
1305
1306        editor = QComboBox(parent)
1307        for opt in self.opts_list:
1308            name     = opt['name']
1309            isActive = opt['active']
1310            isAvail  = opt['available']
1311
1312            editor.addItem(name)
1313            idx = editor.findText(name)
1314            editor.model().item(idx).setEnabled(isActive)
1315            if not isAvail:
1316                editor.setItemData(idx, QColor(Qt.red), Qt.TextColorRole)
1317
1318        editor.installEventFilter(self)
1319
1320        return editor
1321
1322
1323    def setEditorData(self, comboBox, index):
1324
1325        string = from_qvariant(index.model().data(index, Qt.DisplayRole),
1326                               to_text_string)
1327        comboBox.setEditText(string)
1328
1329    def setModelData(self, comboBox, model, index):
1330
1331        value = comboBox.currentText()
1332
1333        selectionModel = self.parent.selectionModel()
1334
1335        for idx in selectionModel.selectedIndexes():
1336            if idx.column() == index.column():
1337                model.setData(idx, value, Qt.DisplayRole)
1338
1339
1340class BasicTableModel(QAbstractTableModel):
1341
1342    def __init__(self, parent=QModelIndex(), xml_model=None, data=[[]], headers=[], default_row=[]):
1343        super(BasicTableModel, self).__init__(parent)
1344        self.parent = parent
1345        self.xml_model = xml_model
1346        self.data_table = data
1347        self.headers = headers
1348        self.default_row = default_row
1349
1350    def rowCount(self, parent=QModelIndex()):
1351        return len(self.data_table)
1352
1353    def columnCount(self, parent=QModelIndex()):
1354        return len(self.headers)
1355
1356    def data(self, index, role=Qt.DisplayRole):
1357        if not index.isValid():
1358            return QVariant()
1359        elif role != Qt.DisplayRole:
1360            return QVariant()
1361        return self.data_table[index.row()][index.column()]
1362
1363    def headerData(self, section, orientation, role=Qt.DisplayRole):
1364        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
1365            return self.headers[section]
1366        return None
1367
1368    def flags(self, index):
1369        if not index.isValid():
1370            return Qt.NoItemFlags
1371        else:
1372            return Qt.ItemIsEnabled | Qt.ItemIsSelectable
1373
1374    def setData(self, index, value, role=Qt.EditRole):
1375        if (role == Qt.EditRole) and (index.isValid()):
1376            self.data_table[index.row()][index.column()] = value
1377            self.dataChanged.emit(index, index)
1378            return True
1379        return QAbstractTableModel.setData(index, value, role)
1380
1381    def insertRows(self, position, rows=1, index=QModelIndex()):
1382        self.beginInsertRows(QModelIndex(), position, position + rows - 1)
1383        for row in range(rows):
1384            self.data_table.insert(position + row, self.default_row)
1385        self.endInsertRows()
1386        return True
1387
1388    def removeRows(self, position, rows=1, index=QModelIndex()):
1389        self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
1390        del self.data_table[position:position + rows]
1391        self.endRemoveRows()
1392        return True
1393
1394# -------------------------------------------------------------------------------
1395# End of QtPage
1396# -------------------------------------------------------------------------------
1397