1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a tree widget for the AdBlock configuration dialog.
8"""
9
10from PyQt5.QtCore import Qt
11from PyQt5.QtGui import QFont, QColor
12from PyQt5.QtWidgets import (
13    QAbstractItemView, QTreeWidgetItem, QInputDialog, QLineEdit, QMenu,
14    QApplication
15)
16
17from E5Gui.E5TreeWidget import E5TreeWidget, E5TreeWidgetItemsState
18from E5Gui.E5OverrideCursor import E5OverrideCursor
19
20
21class AdBlockTreeWidget(E5TreeWidget):
22    """
23    Class implementing a tree widget for the AdBlock configuration dialog.
24    """
25    def __init__(self, subscription, parent=None):
26        """
27        Constructor
28
29        @param subscription reference to the subscription
30        @type AdBlockSubscription
31        @param parent reference to the parent widget
32        @type QWidget
33        """
34        super().__init__(parent)
35
36        self.__subscription = subscription
37        self.__topItem = None
38        self.__ruleToBeSelected = ""
39        self.__itemChangingBlock = False
40
41        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
42        self.setDefaultItemShowMode(E5TreeWidgetItemsState.EXPANDED)
43        self.setHeaderHidden(True)
44        self.setAlternatingRowColors(True)
45
46        self.customContextMenuRequested.connect(self.__contextMenuRequested)
47        self.itemChanged.connect(self.__itemChanged)
48        self.__subscription.changed.connect(self.__subscriptionChanged)
49        self.__subscription.rulesChanged.connect(self.__subscriptionChanged)
50
51    def subscription(self):
52        """
53        Public method to get a reference to the subscription.
54
55        @return reference to the subscription
56        @rtype AdBlockSubscription
57        """
58        return self.__subscription
59
60    def showRule(self, rule):
61        """
62        Public method to highlight the given rule.
63
64        @param rule AdBlock rule to be shown
65        @type AdBlockRule
66        """
67        if not bool(self.__topItem) and bool(rule):
68            self.__ruleToBeSelected = rule.filter()
69        elif self.__ruleToBeSelected:
70            items = self.findItems(
71                self.__ruleToBeSelected, Qt.MatchFlag.MatchRecursive)
72            if items:
73                item = items[0]
74                self.setCurrentItem(item)
75                self.scrollToItem(
76                    item, QAbstractItemView.ScrollHint.PositionAtCenter)
77
78            self.__ruleToBeSelected = ""
79
80    def refresh(self):
81        """
82        Public method to refresh the tree.
83        """
84        with E5OverrideCursor():
85            self.__itemChangingBlock = True
86            self.clear()
87
88            boldFont = QFont()
89            boldFont.setBold(True)
90
91            self.__topItem = QTreeWidgetItem(self)
92            self.__topItem.setText(0, self.__subscription.title())
93            self.__topItem.setFont(0, boldFont)
94            self.addTopLevelItem(self.__topItem)
95
96            allRules = self.__subscription.allRules()
97
98            for index, rule in enumerate(allRules):
99                item = QTreeWidgetItem(self.__topItem)
100                item.setText(0, rule.filter())
101                item.setData(0, Qt.ItemDataRole.UserRole, index)
102                if self.__subscription.canEditRules():
103                    item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable)
104                self.__adjustItemFeatures(item, rule)
105
106            self.expandAll()
107            self.showRule(None)
108            self.__itemChangingBlock = False
109
110    def addRule(self, filterRule=""):
111        """
112        Public slot to add a new rule.
113
114        @param filterRule filter to be added
115        @type str
116        """
117        if not self.__subscription.canEditRules():
118            return
119
120        if not filterRule:
121            filterRule, ok = QInputDialog.getText(
122                self,
123                self.tr("Add Custom Rule"),
124                self.tr("Write your rule here:"),
125                QLineEdit.EchoMode.Normal)
126            if not ok or filterRule == "":
127                return
128
129        from .AdBlockRule import AdBlockRule
130        rule = AdBlockRule(filterRule, self.__subscription)
131        self.__subscription.addRule(rule)
132
133    def removeRule(self):
134        """
135        Public slot to remove the current rule.
136        """
137        item = self.currentItem()
138        if (
139            item is None or
140            not self.__subscription.canEditRules() or
141            item == self.__topItem
142        ):
143            return
144
145        offset = item.data(0, Qt.ItemDataRole.UserRole)
146        self.__subscription.removeRule(offset)
147        self.deleteItem(item)
148
149    def __contextMenuRequested(self, pos):
150        """
151        Private slot to show the context menu.
152
153        @param pos position for the menu
154        @type QPoint
155        """
156        if not self.__subscription.canEditRules():
157            return
158
159        item = self.itemAt(pos)
160        if item is None:
161            return
162
163        menu = QMenu()
164        menu.addAction(self.tr("Add Rule"), self.addRule)
165        menu.addSeparator()
166        act = menu.addAction(self.tr("Remove Rule"), self.removeRule)
167        if item.parent() is None:
168            act.setDisabled(True)
169
170        menu.exec(self.viewport().mapToGlobal(pos))
171
172    def __itemChanged(self, itm):
173        """
174        Private slot to handle the change of an item.
175
176        @param itm changed item
177        @type QTreeWidgetItem
178        """
179        if itm is None or self.__itemChangingBlock:
180            return
181
182        self.__itemChangingBlock = True
183
184        offset = itm.data(0, Qt.ItemDataRole.UserRole)
185        oldRule = self.__subscription.rule(offset)
186
187        if (
188            itm.checkState(0) == Qt.CheckState.Unchecked and
189            oldRule.isEnabled()
190        ):
191            # Disable rule
192            rule = self.__subscription.setRuleEnabled(offset, False)
193            self.__adjustItemFeatures(itm, rule)
194        elif (
195            itm.checkState(0) == Qt.CheckState.Checked and
196            not oldRule.isEnabled()
197        ):
198            # Enable rule
199            rule = self.__subscription.setRuleEnabled(offset, True)
200            self.__adjustItemFeatures(itm, rule)
201        elif self.__subscription.canEditRules():
202            from .AdBlockRule import AdBlockRule
203            # Custom rule has been changed
204            rule = self.__subscription.replaceRule(
205                AdBlockRule(itm.text(0), self.__subscription), offset)
206            self.__adjustItemFeatures(itm, rule)
207
208        self.__itemChangingBlock = False
209
210    def __copyFilter(self):
211        """
212        Private slot to copy the current filter to the clipboard.
213        """
214        item = self.currentItem()
215        if item is not None:
216            QApplication.clipboard().setText(item.text(0))
217
218    def __subscriptionChanged(self):
219        """
220        Private slot handling a subscription change.
221        """
222        self.refresh()
223
224        self.__itemChangingBlock = True
225        self.__topItem.setText(
226            0, self.tr("{0} (recently updated)").format(
227                self.__subscription.title()))
228        self.__itemChangingBlock = False
229
230    def __adjustItemFeatures(self, itm, rule):
231        """
232        Private method to adjust an item.
233
234        @param itm item to be adjusted
235        @type QTreeWidgetItem
236        @param rule rule for the adjustment
237        @type AdBlockRule
238        """
239        if not rule.isEnabled():
240            font = QFont()
241            font.setItalic(True)
242            itm.setForeground(0, QColor(Qt.GlobalColor.gray))
243
244            if not rule.isComment() and not rule.isHeader():
245                itm.setFlags(itm.flags() | Qt.ItemFlag.ItemIsUserCheckable)
246                itm.setCheckState(0, Qt.CheckState.Unchecked)
247                itm.setFont(0, font)
248
249            return
250
251        itm.setFlags(itm.flags() | Qt.ItemFlag.ItemIsUserCheckable)
252        itm.setCheckState(0, Qt.CheckState.Checked)
253
254        if rule.isCSSRule():
255            itm.setForeground(0, QColor(Qt.GlobalColor.darkBlue))
256            itm.setFont(0, QFont())
257        elif rule.isException():
258            itm.setForeground(0, QColor(Qt.GlobalColor.darkGreen))
259            itm.setFont(0, QFont())
260        else:
261            itm.setForeground(0, QColor())
262            itm.setFont(0, QFont())
263
264    def keyPressEvent(self, evt):
265        """
266        Protected method handling key presses.
267
268        @param evt key press event
269        @type QKeyEvent
270        """
271        if (
272            evt.key() == Qt.Key.Key_C and
273            evt.modifiers() & Qt.KeyboardModifier.ControlModifier
274        ):
275            self.__copyFilter()
276        elif evt.key() == Qt.Key.Key_Delete:
277            self.removeRule()
278        else:
279            super().keyPressEvent(evt)
280