1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2006 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the Editor Highlighting Styles configuration page.
8"""
9
10import os
11
12from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo, QFile, QIODevice
13from PyQt5.QtGui import QPalette, QFont, QColor
14from PyQt5.QtWidgets import (
15    QColorDialog, QFontDialog, QInputDialog, QMenu, QTreeWidgetItem, QDialog
16)
17
18from .ConfigurationPageBase import ConfigurationPageBase
19from .Ui_EditorHighlightingStylesPage import Ui_EditorHighlightingStylesPage
20from ..SubstyleDefinitionDialog import SubstyleDefinitionDialog
21
22from E5Gui import E5MessageBox, E5FileDialog
23
24import UI.PixmapCache
25
26try:
27    MonospacedFontsOption = QFontDialog.FontDialogOption.MonospacedFonts
28except AttributeError:
29    MonospacedFontsOption = QFontDialog.FontDialogOptions(0x10)
30NoFontsOption = QFontDialog.FontDialogOptions(0)
31
32
33class EditorHighlightingStylesPage(ConfigurationPageBase,
34                                   Ui_EditorHighlightingStylesPage):
35    """
36    Class implementing the Editor Highlighting Styles configuration page.
37    """
38    FAMILYONLY = 0
39    SIZEONLY = 1
40    FAMILYANDSIZE = 2
41    FONT = 99
42
43    StyleRole = Qt.ItemDataRole.UserRole + 1
44    SubstyleRole = Qt.ItemDataRole.UserRole + 2
45
46    def __init__(self, lexers):
47        """
48        Constructor
49
50        @param lexers reference to the lexers dictionary
51        """
52        super().__init__()
53        self.setupUi(self)
54        self.setObjectName("EditorHighlightingStylesPage")
55
56        self.defaultSubstylesButton.setIcon(UI.PixmapCache.getIcon("editUndo"))
57        self.addSubstyleButton.setIcon(UI.PixmapCache.getIcon("plus"))
58        self.deleteSubstyleButton.setIcon(UI.PixmapCache.getIcon("minus"))
59        self.editSubstyleButton.setIcon(UI.PixmapCache.getIcon("edit"))
60        self.copySubstyleButton.setIcon(UI.PixmapCache.getIcon("editCopy"))
61
62        self.__fontButtonMenu = QMenu()
63        act = self.__fontButtonMenu.addAction(self.tr("Font"))
64        act.setData(self.FONT)
65        self.__fontButtonMenu.addSeparator()
66        act = self.__fontButtonMenu.addAction(
67            self.tr("Family and Size only"))
68        act.setData(self.FAMILYANDSIZE)
69        act = self.__fontButtonMenu.addAction(self.tr("Family only"))
70        act.setData(self.FAMILYONLY)
71        act = self.__fontButtonMenu.addAction(self.tr("Size only"))
72        act.setData(self.SIZEONLY)
73        self.__fontButtonMenu.triggered.connect(self.__fontButtonMenuTriggered)
74        self.fontButton.setMenu(self.__fontButtonMenu)
75
76        self.__allFontsButtonMenu = QMenu()
77        act = self.__allFontsButtonMenu.addAction(self.tr("Font"))
78        act.setData(self.FONT)
79        self.__allFontsButtonMenu.addSeparator()
80        act = self.__allFontsButtonMenu.addAction(
81            self.tr("Family and Size only"))
82        act.setData(self.FAMILYANDSIZE)
83        act = self.__allFontsButtonMenu.addAction(self.tr("Family only"))
84        act.setData(self.FAMILYONLY)
85        act = self.__allFontsButtonMenu.addAction(self.tr("Size only"))
86        act.setData(self.SIZEONLY)
87        self.__allFontsButtonMenu.triggered.connect(
88            self.__allFontsButtonMenuTriggered)
89        self.allFontsButton.setMenu(self.__allFontsButtonMenu)
90
91        self.lexer = None
92        self.lexers = lexers
93
94        # set initial values
95        import QScintilla.Lexers
96        languages = sorted([''] + list(self.lexers.keys()))
97        for language in languages:
98            self.lexerLanguageComboBox.addItem(
99                QScintilla.Lexers.getLanguageIcon(language, False),
100                language)
101        self.on_lexerLanguageComboBox_activated(0)
102
103    def save(self):
104        """
105        Public slot to save the Editor Highlighting Styles configuration.
106        """
107        for lexer in list(self.lexers.values()):
108            lexer.writeSettings()
109
110    @pyqtSlot(int)
111    def on_lexerLanguageComboBox_activated(self, index):
112        """
113        Private slot to fill the style combo of the source page.
114
115        @param index index of the selected entry
116        @type int
117        """
118        language = self.lexerLanguageComboBox.itemText(index)
119
120        self.styleElementList.clear()
121        self.styleGroup.setEnabled(False)
122        self.lexer = None
123
124        if not language:
125            return
126
127        try:
128            self.lexer = self.lexers[language]
129        except KeyError:
130            return
131
132        self.styleGroup.setEnabled(True)
133        for description, styleNo, subStyleNo in self.lexer.getStyles():
134            if subStyleNo >= 0:
135                parent = self.styleElementList.findItems(
136                    self.lexer.description(styleNo),
137                    Qt.MatchFlag.MatchExactly)[0]
138                parent.setExpanded(True)
139            else:
140                parent = self.styleElementList
141            itm = QTreeWidgetItem(parent, [description])
142            itm.setData(0, self.StyleRole, styleNo)
143            itm.setData(0, self.SubstyleRole, subStyleNo)
144        self.__styleAllItems()
145        self.styleElementList.setCurrentItem(
146            self.styleElementList.topLevelItem(0))
147
148    def __stylesForItem(self, itm):
149        """
150        Private method to get the style and sub-style number of the given item.
151
152        @param itm reference to the item to extract the styles from
153        @type QTreeWidgetItem
154        @return tuple containing the style and sub-style numbers
155        @rtype tuple of (int, int)
156        """
157        style = itm.data(0, self.StyleRole)
158        substyle = itm.data(0, self.SubstyleRole)
159
160        return (style, substyle)
161
162    def __currentStyles(self):
163        """
164        Private method to get the styles of the current item.
165
166        @return tuple containing the style and sub-style numbers
167        @rtype tuple of (int, int)
168        """
169        itm = self.styleElementList.currentItem()
170        # return default style, if no current item
171        styles = (0, -1) if itm is None else self.__stylesForItem(itm)
172
173        return styles
174
175    def __styleOneItem(self, item, style, substyle):
176        """
177        Private method to style one item of the style element list.
178
179        @param item reference to the item to be styled
180        @type QTreeWidgetItem
181        @param style base style number
182        @type int
183        @param substyle sub-style number
184        @type int
185        """
186        colour = self.lexer.color(style, substyle)
187        paper = self.lexer.paper(style, substyle)
188        font = self.lexer.font(style, substyle)
189        eolfill = self.lexer.eolFill(style, substyle)
190
191        item.setFont(0, font)
192        item.setBackground(0, paper)
193        item.setForeground(0, colour)
194        if eolfill:
195            item.setCheckState(0, Qt.CheckState.Checked)
196        else:
197            item.setCheckState(0, Qt.CheckState.Unchecked)
198
199    def __styleAllItems(self):
200        """
201        Private method to style all items of the style element list.
202        """
203        itm = self.styleElementList.topLevelItem(0)
204        while itm is not None:
205            style, substyle = self.__stylesForItem(itm)
206            self.__styleOneItem(itm, style, substyle)
207            itm = self.styleElementList.itemBelow(itm)
208
209    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
210    def on_styleElementList_currentItemChanged(self, current, previous):
211        """
212        Private method to handle a change of the current row.
213
214        @param current reference to the current item
215        @type QTreeWidgetItem
216        @param previous reference to the previous item
217        @type QTreeWidgetItem
218        """
219        if current is None:
220            return
221
222        style, substyle = self.__stylesForItem(current)
223        colour = self.lexer.color(style, substyle)
224        paper = self.lexer.paper(style, substyle)
225        eolfill = self.lexer.eolFill(style, substyle)
226        font = self.lexer.font(style, substyle)
227
228        self.sampleText.setFont(font)
229        pl = self.sampleText.palette()
230        pl.setColor(QPalette.ColorRole.Text, colour)
231        pl.setColor(QPalette.ColorRole.Base, paper)
232        self.sampleText.setPalette(pl)
233        self.sampleText.repaint()
234        self.eolfillCheckBox.setChecked(eolfill)
235
236        selectedOne = len(self.styleElementList.selectedItems()) == 1
237        self.defaultSubstylesButton.setEnabled(
238            selectedOne and substyle < 0 and self.lexer.isBaseStyle(style))
239        self.addSubstyleButton.setEnabled(
240            selectedOne and substyle < 0 and self.lexer.isBaseStyle(style))
241        self.deleteSubstyleButton.setEnabled(selectedOne and substyle >= 0)
242        self.editSubstyleButton.setEnabled(selectedOne and substyle >= 0)
243        self.copySubstyleButton.setEnabled(selectedOne and substyle >= 0)
244
245    @pyqtSlot()
246    def on_foregroundButton_clicked(self):
247        """
248        Private method used to select the foreground colour of the selected
249        style and lexer.
250        """
251        style, substyle = self.__currentStyles()
252        colour = QColorDialog.getColor(self.lexer.color(style, substyle))
253        if colour.isValid():
254            pl = self.sampleText.palette()
255            pl.setColor(QPalette.ColorRole.Text, colour)
256            self.sampleText.setPalette(pl)
257            self.sampleText.repaint()
258            for selItem in self.styleElementList.selectedItems():
259                style, substyle = self.__stylesForItem(selItem)
260                self.lexer.setColor(colour, style, substyle)
261                selItem.setForeground(0, colour)
262
263    @pyqtSlot()
264    def on_backgroundButton_clicked(self):
265        """
266        Private method used to select the background colour of the selected
267        style and lexer.
268        """
269        style, substyle = self.__currentStyles()
270        colour = QColorDialog.getColor(self.lexer.paper(style, substyle))
271        if colour.isValid():
272            pl = self.sampleText.palette()
273            pl.setColor(QPalette.ColorRole.Base, colour)
274            self.sampleText.setPalette(pl)
275            self.sampleText.repaint()
276            for selItem in self.styleElementList.selectedItems():
277                style, substyle = self.__stylesForItem(selItem)
278                self.lexer.setPaper(colour, style, substyle)
279                selItem.setBackground(0, colour)
280
281    @pyqtSlot()
282    def on_allBackgroundColoursButton_clicked(self):
283        """
284        Private method used to select the background colour of all styles of a
285        selected lexer.
286        """
287        style, substyle = self.__currentStyles()
288        colour = QColorDialog.getColor(self.lexer.paper(style, substyle))
289        if colour.isValid():
290            pl = self.sampleText.palette()
291            pl.setColor(QPalette.ColorRole.Base, colour)
292            self.sampleText.setPalette(pl)
293            self.sampleText.repaint()
294
295            itm = self.styleElementList.topLevelItem(0)
296            while itm is not None:
297                style, substyle = self.__stylesForItem(itm)
298                self.lexer.setPaper(colour, style, substyle)
299                itm = self.styleElementList.itemBelow(itm)
300            self.__styleAllItems()
301
302    def __changeFont(self, doAll, familyOnly, sizeOnly):
303        """
304        Private slot to change the highlighter font.
305
306        @param doAll flag indicating to change the font for all styles
307            (boolean)
308        @param familyOnly flag indicating to set the font family only (boolean)
309        @param sizeOnly flag indicating to set the font size only (boolean
310        """
311        def setFont(font, style, substyle, familyOnly, sizeOnly):
312            """
313            Local function to set the font.
314
315            @param font font to be set
316            @type QFont
317            @param style style number
318            @type int
319            @param substyle sub-style number
320            @type int
321            @param familyOnly flag indicating to set the font family only
322            @type bool
323            @param sizeOnly flag indicating to set the font size only
324            @type bool
325            """
326            if familyOnly or sizeOnly:
327                newFont = QFont(self.lexer.font(style))
328                if familyOnly:
329                    newFont.setFamily(font.family())
330                if sizeOnly:
331                    newFont.setPointSize(font.pointSize())
332                self.lexer.setFont(newFont, style, substyle)
333            else:
334                self.lexer.setFont(font, style, substyle)
335
336        def setSampleFont(font, familyOnly, sizeOnly):
337            """
338            Local function to set the font of the sample text.
339
340            @param font font to be set (QFont)
341            @param familyOnly flag indicating to set the font family only
342                (boolean)
343            @param sizeOnly flag indicating to set the font size only (boolean
344            """
345            if familyOnly or sizeOnly:
346                style, substyle = self.__currentStyles()
347                newFont = QFont(self.lexer.font(style, substyle))
348                if familyOnly:
349                    newFont.setFamily(font.family())
350                if sizeOnly:
351                    newFont.setPointSize(font.pointSize())
352                self.sampleText.setFont(newFont)
353            else:
354                self.sampleText.setFont(font)
355
356        style, substyle = self.__currentStyles()
357        options = (
358            MonospacedFontsOption
359            if self.monospacedButton.isChecked() else
360            NoFontsOption
361        )
362        font, ok = QFontDialog.getFont(self.lexer.font(style, substyle), self,
363                                       "", options)
364        if ok:
365            setSampleFont(font, familyOnly, sizeOnly)
366            if doAll:
367                itm = self.styleElementList.topLevelItem(0)
368                while itm is not None:
369                    style, substyle = self.__stylesForItem(itm)
370                    setFont(font, style, substyle, familyOnly, sizeOnly)
371                    itm = self.styleElementList.itemBelow(itm)
372                self.__styleAllItems()
373            else:
374                for selItem in self.styleElementList.selectedItems():
375                    style, substyle = self.__stylesForItem(selItem)
376                    setFont(font, style, substyle, familyOnly, sizeOnly)
377                    itmFont = self.lexer.font(style, substyle)
378                    selItem.setFont(0, itmFont)
379
380    def __fontButtonMenuTriggered(self, act):
381        """
382        Private slot used to select the font of the selected style and lexer.
383
384        @param act reference to the triggering action (QAction)
385        """
386        if act is None:
387            return
388
389        familyOnly = act.data() in [self.FAMILYANDSIZE, self.FAMILYONLY]
390        sizeOnly = act.data() in [self.FAMILYANDSIZE, self.SIZEONLY]
391        self.__changeFont(False, familyOnly, sizeOnly)
392
393    def __allFontsButtonMenuTriggered(self, act):
394        """
395        Private slot used to change the font of all styles of a selected lexer.
396
397        @param act reference to the triggering action (QAction)
398        """
399        if act is None:
400            return
401
402        familyOnly = act.data() in [self.FAMILYANDSIZE, self.FAMILYONLY]
403        sizeOnly = act.data() in [self.FAMILYANDSIZE, self.SIZEONLY]
404        self.__changeFont(True, familyOnly, sizeOnly)
405
406    @pyqtSlot(bool)
407    def on_eolfillCheckBox_clicked(self, on):
408        """
409        Private method used to set the eolfill for the selected style and
410        lexer.
411
412        @param on flag indicating enabled or disabled state (boolean)
413        """
414        style, substyle = self.__currentStyles()
415        checkState = Qt.CheckState.Checked if on else Qt.CheckState.Unchecked
416        for selItem in self.styleElementList.selectedItems():
417            style, substyle = self.__stylesForItem(selItem)
418            self.lexer.setEolFill(on, style, substyle)
419            selItem.setCheckState(0, checkState)
420
421    @pyqtSlot()
422    def on_allEolFillButton_clicked(self):
423        """
424        Private method used to set the eolfill for all styles of a selected
425        lexer.
426        """
427        on = self.tr("Enabled")
428        off = self.tr("Disabled")
429        selection, ok = QInputDialog.getItem(
430            self,
431            self.tr("Fill to end of line"),
432            self.tr("Select fill to end of line for all styles"),
433            [on, off],
434            0, False)
435        if ok:
436            enabled = selection == on
437            self.eolfillCheckBox.setChecked(enabled)
438
439            itm = self.styleElementList.topLevelItem(0)
440            while itm is not None:
441                style, substyle = self.__stylesForItem(itm)
442                self.lexer.setEolFill(enabled, style, substyle)
443                itm = self.styleElementList.itemBelow(itm)
444            self.__styleAllItems()
445
446    @pyqtSlot()
447    def on_defaultButton_clicked(self):
448        """
449        Private method to set the current style to its default values.
450        """
451        for selItem in self.styleElementList.selectedItems():
452            style, substyle = self.__stylesForItem(selItem)
453            self.__setToDefault(style, substyle)
454        self.on_styleElementList_currentItemChanged(
455            self.styleElementList.currentItem(), None)
456        self.__styleAllItems()
457
458    @pyqtSlot()
459    def on_allDefaultButton_clicked(self):
460        """
461        Private method to set all styles to their default values.
462        """
463        itm = self.styleElementList.topLevelItem(0)
464        while itm is not None:
465            style, substyle = self.__stylesForItem(itm)
466            self.__setToDefault(style, substyle)
467            itm = self.styleElementList.itemBelow(itm)
468        self.on_styleElementList_currentItemChanged(
469            self.styleElementList.currentItem(), None)
470        self.__styleAllItems()
471
472    def __setToDefault(self, style, substyle):
473        """
474        Private method to set a specific style to its default values.
475
476        @param style style number
477        @type int
478        @param substyle sub-style number
479        @type int
480        """
481        self.lexer.setColor(self.lexer.defaultColor(style, substyle),
482                            style, substyle)
483        self.lexer.setPaper(self.lexer.defaultPaper(style, substyle),
484                            style, substyle)
485        self.lexer.setFont(self.lexer.defaultFont(style, substyle),
486                           style, substyle)
487        self.lexer.setEolFill(self.lexer.defaultEolFill(style, substyle),
488                              style, substyle)
489
490    #######################################################################
491    ## Importing and exporting of styles
492    #######################################################################
493
494    @pyqtSlot()
495    def on_importButton_clicked(self):
496        """
497        Private slot to import styles to be selected.
498        """
499        self.__importStyles(importAll=False)
500
501    @pyqtSlot()
502    def on_exportButton_clicked(self):
503        """
504        Private slot to export styles to be selected.
505        """
506        self.__exportStyles(exportAll=False)
507
508    @pyqtSlot()
509    def on_importAllButton_clicked(self):
510        """
511        Private slot to import the styles of all lexers.
512        """
513        self.__importStyles(importAll=True)
514
515    @pyqtSlot()
516    def on_exportAllButton_clicked(self):
517        """
518        Private slot to export the styles of all lexers.
519        """
520        self.__exportStyles(exportAll=True)
521
522    def __exportStyles(self, exportAll=False):
523        """
524        Private method to export the styles of selectable lexers.
525
526        @param exportAll flag indicating to export all styles without asking
527            (defaults to False)
528        @type bool (optional)
529        """
530        from eric6config import getConfig
531        stylesDir = getConfig("ericStylesDir")
532
533        lexerNames = list(self.lexers.keys())
534        if not exportAll:
535            if self.lexer:
536                preselect = [self.lexer.language()]
537            else:
538                preselect = []
539            from .EditorHighlightingStylesSelectionDialog import (
540                EditorHighlightingStylesSelectionDialog)
541            dlg = EditorHighlightingStylesSelectionDialog(
542                lexerNames, forImport=False, preselect=preselect)
543            if dlg.exec() == QDialog.DialogCode.Accepted:
544                lexerNames = dlg.getLexerNames()
545            else:
546                # Cancelled by user
547                return
548
549        lexers = [self.lexers[name] for name in lexerNames]
550
551        fn, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
552            self,
553            self.tr("Export Highlighting Styles"),
554            stylesDir,
555            self.tr("Highlighting Styles File (*.ehj);;"
556                    "XML Highlighting Styles File (*.e6h)"),
557            "",
558            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
559
560        if not fn:
561            return
562
563        ext = QFileInfo(fn).suffix()
564        if not ext:
565            ex = selectedFilter.split("(*")[1].split(")")[0]
566            if ex:
567                fn += ex
568
569        ok = (
570            E5MessageBox.yesNo(
571                self,
572                self.tr("Export Highlighting Styles"),
573                self.tr("""<p>The highlighting styles file <b>{0}</b> exists"""
574                        """ already. Overwrite it?</p>""").format(fn))
575            if os.path.exists(fn) else
576            True
577        )
578
579        if ok:
580            if fn.endswith(".ehj"):
581                from Preferences.HighlightingStylesFile import (
582                    HighlightingStylesFile
583                )
584                highlightingStylesFile = HighlightingStylesFile()
585                highlightingStylesFile.writeFile(fn, lexers)
586            else:
587                f = QFile(fn)
588                if f.open(QIODevice.OpenModeFlag.WriteOnly):
589                    from E5XML.HighlightingStylesWriter import (
590                        HighlightingStylesWriter
591                    )
592                    HighlightingStylesWriter(f, lexers).writeXML()
593                    f.close()
594                else:
595                    E5MessageBox.critical(
596                        self,
597                        self.tr("Export Highlighting Styles"),
598                        self.tr("<p>The highlighting styles file <b>{0}</b>"
599                                " could not be written.</p><p>Reason: {1}</p>")
600                        .format(fn, f.errorString())
601                    )
602
603    def __importStyles(self, importAll=False):
604        """
605        Private method to import the styles of lexers to be selected.
606
607        @param importAll flag indicating to import all styles without asking
608            (defaults to False)
609        @type bool (optional)
610        """
611        from eric6config import getConfig
612        stylesDir = getConfig("ericStylesDir")
613
614        fn = E5FileDialog.getOpenFileName(
615            self,
616            self.tr("Import Highlighting Styles"),
617            stylesDir,
618            self.tr("Highlighting Styles File (*.ehj);;"
619                    "XML Highlighting Styles File (*.e6h *.e4h)"))
620
621        if not fn:
622            return
623
624        if fn.endswith(".ehj"):
625            # new JSON based file
626            from Preferences.HighlightingStylesFile import (
627                HighlightingStylesFile
628            )
629            highlightingStylesFile = HighlightingStylesFile()
630            styles = highlightingStylesFile.readFile(fn)
631            if not styles:
632                return
633        else:
634            # old XML based file
635            f = QFile(fn)
636            if f.open(QIODevice.OpenModeFlag.ReadOnly):
637                from E5XML.HighlightingStylesReader import (
638                    HighlightingStylesReader
639                )
640                reader = HighlightingStylesReader(f, self.lexers)
641                styles = reader.readXML()
642                f.close()
643                if not styles:
644                    return
645            else:
646                E5MessageBox.critical(
647                    self,
648                    self.tr("Import Highlighting Styles"),
649                    self.tr(
650                        "<p>The highlighting styles file <b>{0}</b> could not"
651                        " be read.</p><p>Reason: {1}</p>"
652                    ).format(fn, f.errorString())
653                )
654                return
655
656        self.__applyStyles(styles, importAll=importAll)
657        self.on_lexerLanguageComboBox_activated(
658            self.lexerLanguageComboBox.currentIndex())
659
660    def __applyStyles(self, stylesList, importAll=False):
661        """
662        Private method to apply the imported styles to this dialog.
663
664        @param stylesList list of imported lexer styles
665        @type list of dict
666        @param importAll flag indicating to import all styles without asking
667            (defaults to False)
668        @type bool (optional)
669        """
670        lexerNames = [d["name"]
671                      for d in stylesList
672                      if d["name"] in self.lexers]
673
674        if not importAll:
675            from .EditorHighlightingStylesSelectionDialog import (
676                EditorHighlightingStylesSelectionDialog)
677            dlg = EditorHighlightingStylesSelectionDialog(
678                lexerNames, forImport=True)
679            if dlg.exec() == QDialog.DialogCode.Accepted:
680                lexerNames = dlg.getLexerNames()
681            else:
682                # Cancelled by user
683                return
684
685        for lexerDict in stylesList:
686            if lexerDict["name"] in lexerNames:
687                lexer = self.lexers[lexerDict["name"]]
688                for styleDict in lexerDict["styles"]:
689                    style = styleDict["style"]
690                    substyle = styleDict["substyle"]
691                    lexer.setColor(QColor(styleDict["color"]), style, substyle)
692                    lexer.setPaper(QColor(styleDict["paper"]), style, substyle)
693                    font = QFont()
694                    font.fromString(styleDict["font"])
695                    lexer.setFont(font, style, substyle)
696                    lexer.setEolFill(styleDict["eolfill"], style, substyle)
697                    if substyle >= 0:
698                        # description and words can only be set for sub-styles
699                        lexer.setDescription(styleDict["description"],
700                                             style, substyle)
701                        lexer.setWords(styleDict["words"], style, substyle)
702
703    #######################################################################
704    ## Methods to save and restore the state
705    #######################################################################
706
707    def saveState(self):
708        """
709        Public method to save the current state of the widget.
710
711        @return list containing the index of the selected lexer language
712            and a tuple containing the index of the parent selected lexer
713            entry and the index of the selected entry
714        @rtype list of int and tuple of (int, int)
715        """
716        itm = self.styleElementList.currentItem()
717        if itm:
718            parent = itm.parent()
719            if parent is None:
720                currentData = (
721                    None, self.styleElementList.indexOfTopLevelItem(itm))
722            else:
723                currentData = (
724                    self.styleElementList.indexOfTopLevelItem(parent),
725                    parent.indexOfChild(itm)
726                )
727
728            savedState = [
729                self.lexerLanguageComboBox.currentIndex(),
730                currentData,
731            ]
732        else:
733            savedState = []
734        return savedState
735
736    def setState(self, state):
737        """
738        Public method to set the state of the widget.
739
740        @param state state data generated by saveState
741        """
742        if state:
743            self.lexerLanguageComboBox.setCurrentIndex(state[0])
744            self.on_lexerLanguageComboBox_activated(
745                self.lexerLanguageComboBox.currentIndex())
746
747            parentIndex, index = state[1]
748            if parentIndex is None:
749                itm = self.styleElementList.topLevelItem(index)
750            else:
751                parent = self.styleElementList.topLevelItem(parentIndex)
752                itm = parent.child(index)
753            self.styleElementList.setCurrentItem(itm)
754
755    #######################################################################
756    ## Methods to add, delete and edit sub-styles and their definitions
757    #######################################################################
758
759    @pyqtSlot()
760    def on_addSubstyleButton_clicked(self):
761        """
762        Private slot to add a new sub-style.
763        """
764        style, substyle = self.__currentStyles()
765        dlg = SubstyleDefinitionDialog(
766            self.lexer, style, substyle, parent=self)
767        if dlg.exec() == QDialog.DialogCode.Accepted:
768            description, words = dlg.getData()
769            substyle = self.lexer.addSubstyle(style)
770            self.lexer.setDescription(description, style, substyle)
771            self.lexer.setWords(words, style, substyle)
772
773            parent = self.styleElementList.findItems(
774                self.lexer.description(style), Qt.MatchFlag.MatchExactly)[0]
775            parent.setExpanded(True)
776            itm = QTreeWidgetItem(parent, [description])
777            itm.setData(0, self.StyleRole, style)
778            itm.setData(0, self.SubstyleRole, substyle)
779            self.__styleOneItem(itm, style, substyle)
780
781    @pyqtSlot()
782    def on_deleteSubstyleButton_clicked(self):
783        """
784        Private slot to delete the selected sub-style.
785        """
786        style, substyle = self.__currentStyles()
787        ok = E5MessageBox.yesNo(
788            self,
789            self.tr("Delete Sub-Style"),
790            self.tr("""<p>Shall the sub-style <b>{0}</b> really be"""
791                    """ deleted?</p>""").format(
792                self.lexer.description(style, substyle))
793        )
794        if ok:
795            self.lexer.delSubstyle(style, substyle)
796
797            itm = self.styleElementList.currentItem()
798            parent = itm.parent()
799            index = parent.indexOfChild(itm)
800            parent.takeChild(index)
801            del itm
802
803    @pyqtSlot()
804    def on_editSubstyleButton_clicked(self):
805        """
806        Private slot to edit the selected sub-style entry.
807        """
808        style, substyle = self.__currentStyles()
809        dlg = SubstyleDefinitionDialog(
810            self.lexer, style, substyle, parent=self)
811        if dlg.exec() == QDialog.DialogCode.Accepted:
812            description, words = dlg.getData()
813            self.lexer.setDescription(description, style, substyle)
814            self.lexer.setWords(words, style, substyle)
815
816            itm = self.styleElementList.currentItem()
817            itm.setText(0, description)
818
819    @pyqtSlot()
820    def on_copySubstyleButton_clicked(self):
821        """
822        Private slot to copy the selected sub-style.
823        """
824        style, substyle = self.__currentStyles()
825        newSubstyle = self.lexer.addSubstyle(style)
826
827        description = self.tr("{0} - Copy").format(
828            self.lexer.description(style, substyle))
829        self.lexer.setDescription(description, style, newSubstyle)
830        self.lexer.setWords(self.lexer.words(style, substyle),
831                            style, newSubstyle)
832        self.lexer.setColor(self.lexer.color(style, substyle),
833                            style, newSubstyle)
834        self.lexer.setPaper(self.lexer.paper(style, substyle),
835                            style, newSubstyle)
836        self.lexer.setFont(self.lexer.font(style, substyle),
837                           style, newSubstyle)
838        self.lexer.setEolFill(self.lexer.eolFill(style, substyle),
839                              style, newSubstyle)
840
841        parent = self.styleElementList.findItems(
842            self.lexer.description(style), Qt.MatchFlag.MatchExactly)[0]
843        parent.setExpanded(True)
844        itm = QTreeWidgetItem(parent, [description])
845        itm.setData(0, self.StyleRole, style)
846        itm.setData(0, self.SubstyleRole, newSubstyle)
847        self.__styleOneItem(itm, style, newSubstyle)
848
849    @pyqtSlot()
850    def on_defaultSubstylesButton_clicked(self):
851        """
852        Private slot to reset all substyles to default values.
853        """
854        style, substyle = self.__currentStyles()
855        ok = E5MessageBox.yesNo(
856            self,
857            self.tr("Reset Sub-Styles to Default"),
858            self.tr("<p>Do you really want to reset all defined sub-styles of"
859                    " <b>{0}</b> to the default values?</p>""")
860            .format(self.lexer.description(style, substyle))
861        )
862        if ok:
863            # 1. reset sub-styles
864            self.lexer.loadDefaultSubStyles(style)
865
866            # 2. delete all existing sub-style items
867            parent = self.styleElementList.currentItem()
868            while parent.childCount() > 0:
869                itm = parent.takeChild(0)     # __IGNORE_WARNING__
870                del itm
871
872            # 3. create the new list of sub-style items
873            for description, _, substyle in self.lexer.getSubStyles(style):
874                itm = QTreeWidgetItem(parent, [description])
875                itm.setData(0, self.StyleRole, style)
876                itm.setData(0, self.SubstyleRole, substyle)
877                self.__styleOneItem(itm, style, substyle)
878
879
880def create(dlg):
881    """
882    Module function to create the configuration page.
883
884    @param dlg reference to the configuration dialog
885    @return reference to the instantiated page (ConfigurationPageBase)
886    """
887    page = EditorHighlightingStylesPage(dlg.getLexers())
888    return page
889