GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

__paintEventNoStyle()   F
last analyzed

Complexity

Conditions 13

Size

Total Lines 105

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 13
dl 0
loc 105
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Orange.canvas.gui.ToolBoxTabButton.__paintEventNoStyle() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
2
==============
3
Tool Box Widget
4
==============
5
6
A reimplementation of the :class:`QToolBox` widget that keeps all the tabs
7
in a single :class:`QScrollArea` instance and can keep multiple open tabs.
8
9
"""
10
11
from collections import namedtuple
12
from operator import eq, attrgetter
13
14
from PyQt4.QtGui import (
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtGui could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
15
    QWidget, QFrame, QSizePolicy, QIcon, QFontMetrics, QPainter, QStyle,
16
    QStyleOptionToolButton, QStyleOptionToolBoxV2, QPalette, QBrush, QPen,
17
    QColor, QScrollArea, QVBoxLayout, QToolButton, QAction, QActionGroup
18
)
19
20
from PyQt4.QtCore import (
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
21
    Qt, QObject, QSize, QRect, QPoint, QSignalMapper, QEvent
22
)
23
24
from PyQt4.QtCore import pyqtSignal as Signal, pyqtProperty as Property
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
25
26
from .utils import brush_darker
27
28
_ToolBoxPage = namedtuple(
29
    "_ToolBoxPage",
30
    ["index",
31
     "widget",
32
     "action",
33
     "button"]
34
    )
35
36
37
FOCUS_OUTLINE_COLOR = "#609ED7"
38
39
40
class ToolBoxTabButton(QToolButton):
41
    """
42
    A tab button for an item in a :class:`ToolBox`.
43
    """
44
45
    def setNativeStyling(self, state):
46
        """
47
        Render tab buttons as native (or css styled) :class:`QToolButtons`.
48
        If set to `False` (default) the button is pained using a custom
49
        paint routine.
50
51
        """
52
        self.__nativeStyling = state
53
        self.update()
54
55
    def nativeStyling(self):
56
        """
57
        Use :class:`QStyle`'s to paint the class:`QToolButton` look.
58
        """
59
        return self.__nativeStyling
60
61
    nativeStyling_ = Property(bool,
62
                              fget=nativeStyling,
63
                              fset=setNativeStyling,
64
                              designable=True)
65
66
    def __init__(self, *args, **kwargs):
67
        self.__nativeStyling = False
68
        self.position = QStyleOptionToolBoxV2.OnlyOneTab
69
        self.selected = QStyleOptionToolBoxV2.NotAdjacent
70
71
        QToolButton.__init__(self, *args, **kwargs)
72
73
    def paintEvent(self, event):
74
        if self.__nativeStyling:
75
            QToolButton.paintEvent(self, event)
76
        else:
77
            self.__paintEventNoStyle()
78
79
    def __paintEventNoStyle(self):
80
        p = QPainter(self)
81
        opt = QStyleOptionToolButton()
82
        self.initStyleOption(opt)
83
84
        fm = QFontMetrics(opt.font)
85
        palette = opt.palette
86
87
        # highlight brush is used as the background for the icon and background
88
        # when the tab is expanded and as mouse hover color (lighter).
89
        brush_highlight = palette.highlight()
90
        if opt.state & QStyle.State_Sunken:
91
            # State 'down' pressed during a mouse press (slightly darker).
92
            background_brush = brush_darker(brush_highlight, 110)
93
        elif opt.state & QStyle.State_MouseOver:
94
            background_brush = brush_darker(brush_highlight, 95)
95
        elif opt.state & QStyle.State_On:
96
            background_brush = brush_highlight
97
        else:
98
            # The default button brush.
99
            background_brush = palette.button()
100
101
        rect = opt.rect
102
        icon = opt.icon
103
        icon_size = opt.iconSize
104
105
        # TODO: add shift for pressed as set by the style (PM_ButtonShift...)
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
106
107
        pm = None
108
        if not icon.isNull():
109
            if opt.state & QStyle.State_Enabled:
110
                mode = QIcon.Normal
111
            else:
112
                mode = QIcon.Disabled
113
114
            pm = opt.icon.pixmap(
115
                    rect.size().boundedTo(icon_size), mode,
116
                    QIcon.On if opt.state & QStyle.State_On else QIcon.Off)
117
118
        icon_area_rect = QRect(rect)
119
        icon_area_rect.setRight(int(icon_area_rect.height() * 1.26))
120
121
        text_rect = QRect(rect)
122
        text_rect.setLeft(icon_area_rect.right() + 10)
123
124
        # Background  (TODO: Should the tab button have native
125
        # toolbutton shape, drawn using PE_PanelButtonTool or even
126
        # QToolBox tab shape)
127
128
        # Default outline pen
129
        pen = QPen(palette.color(QPalette.Mid))
130
131
        p.save()
132
        p.setPen(Qt.NoPen)
133
        p.setBrush(QBrush(background_brush))
134
        p.drawRect(rect)
135
136
        # Draw the background behind the icon if the background_brush
137
        # is different.
138
        if not opt.state & QStyle.State_On:
139
            p.setBrush(brush_highlight)
140
            p.drawRect(icon_area_rect)
141
            # Line between the icon and text
142
            p.setPen(pen)
143
            p.drawLine(icon_area_rect.topRight(),
144
                       icon_area_rect.bottomRight())
145
146
        if opt.state & QStyle.State_HasFocus:
147
            # Set the focus frame pen and draw the border
148
            pen = QPen(QColor(FOCUS_OUTLINE_COLOR))
149
            p.setPen(pen)
150
            p.setBrush(Qt.NoBrush)
151
            # Adjust for pen
152
            rect = rect.adjusted(0, 0, -1, -1)
153
            p.drawRect(rect)
154
155
        else:
156
            p.setPen(pen)
157
            # Draw the top/bottom border
158
            if self.position == QStyleOptionToolBoxV2.OnlyOneTab or \
159
                    self.position == QStyleOptionToolBoxV2.Beginning or \
160
                    self.selected & \
161
                        QStyleOptionToolBoxV2.PreviousIsSelected:
162
163
                p.drawLine(rect.topLeft(), rect.topRight())
164
165
            p.drawLine(rect.bottomLeft(), rect.bottomRight())
166
167
        p.restore()
168
169
        p.save()
170
        text = fm.elidedText(opt.text, Qt.ElideRight, text_rect.width())
171
        p.setPen(QPen(palette.color(QPalette.ButtonText)))
172
        p.setFont(opt.font)
173
174
        p.drawText(text_rect,
175
                   int(Qt.AlignVCenter | Qt.AlignLeft) | \
176
                   int(Qt.TextSingleLine),
177
                   text)
178
        if pm:
179
            pm_rect = QRect(QPoint(0, 0), pm.size())
180
            centered_rect = QRect(pm_rect)
181
            centered_rect.moveCenter(icon_area_rect.center())
182
            p.drawPixmap(centered_rect, pm, pm_rect)
183
        p.restore()
184
185
186
class _ToolBoxScrollArea(QScrollArea):
187
    def eventFilter(self, obj, event):
188
        if obj is self.widget() and event.type() == QEvent.Resize:
189
            if event.size() == event.oldSize() and self.widgetResizable():
190
                # This is driving me insane. This should not have happened.
191
                # Before the event is sent QWidget specifically makes sure the
192
                # sizes are different, but somehow I still get this, and enter
193
                # an infinite recursion if I enter QScrollArea.eventFilter.
194
                # I can only duplicate this on one development machine a
195
                # Mac OSX using fink and Qt 4.7.3
196
                return False
197
198
        return QScrollArea.eventFilter(self, obj, event)
199
200
201
class ToolBox(QFrame):
202
    """
203
    A tool box widget.
204
    """
205
    # Emitted when a tab is toggled.
206
    tabToogled = Signal(int, bool)
207
208
    def setExclusive(self, exclusive):
209
        """
210
        Set exclusive tabs (only one tab can be open at a time).
211
        """
212
        if self.__exclusive != exclusive:
213
            self.__exclusive = exclusive
214
            self.__tabActionGroup.setExclusive(exclusive)
215
            checked = self.__tabActionGroup.checkedAction()
216
            if checked is None:
217
                # The action group can be out of sync with the actions state
218
                # when switching between exclusive states.
219
                actions_checked = [page.action for page in self.__pages
220
                                   if page.action.isChecked()]
221
                if actions_checked:
222
                    checked = actions_checked[0]
223
224
            # Trigger/toggle remaining open pages
225
            if exclusive and checked is not None:
226
                for page in self.__pages:
227
                    if checked != page.action and page.action.isChecked():
228
                        page.action.trigger()
229
230
    def exclusive(self):
231
        """
232
        Are the tabs in the toolbox exclusive.
233
        """
234
        return self.__exclusive
235
236
    exclusive_ = Property(bool,
237
                          fget=exclusive,
238
                          fset=setExclusive,
239
                          designable=True,
240
                          doc="Exclusive tabs")
241
242
    def __init__(self, parent=None, **kwargs):
243
        QFrame.__init__(self, parent, **kwargs)
244
245
        self.__pages = []
246
        self.__tabButtonHeight = -1
247
        self.__tabIconSize = QSize()
248
        self.__exclusive = False
249
        self.__setupUi()
250
251
    def __setupUi(self):
252
        layout = QVBoxLayout()
253
        layout.setContentsMargins(0, 0, 0, 0)
254
255
        # Scroll area for the contents.
256
        self.__scrollArea = \
257
                _ToolBoxScrollArea(self, objectName="toolbox-scroll-area")
258
259
        self.__scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
260
        self.__scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
261
        self.__scrollArea.setSizePolicy(QSizePolicy.MinimumExpanding,
262
                                        QSizePolicy.MinimumExpanding)
263
        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)
264
        self.__scrollArea.setWidgetResizable(True)
265
266
        # A widget with all of the contents.
267
        # The tabs/contents are placed in the layout inside this widget
268
        self.__contents = QWidget(self.__scrollArea,
269
                                  objectName="toolbox-contents")
270
271
        # The layout where all the tab/pages are placed
272
        self.__contentsLayout = QVBoxLayout()
273
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
274
        self.__contentsLayout.setSizeConstraint(QVBoxLayout.SetMinAndMaxSize)
275
        self.__contentsLayout.setSpacing(0)
276
277
        self.__contents.setLayout(self.__contentsLayout)
278
279
        self.__scrollArea.setWidget(self.__contents)
280
281
        layout.addWidget(self.__scrollArea)
282
283
        self.setLayout(layout)
284
        self.setSizePolicy(QSizePolicy.Fixed,
285
                           QSizePolicy.MinimumExpanding)
286
287
        self.__tabActionGroup = \
288
                QActionGroup(self, objectName="toolbox-tab-action-group")
289
290
        self.__tabActionGroup.setExclusive(self.__exclusive)
291
292
        self.__actionMapper = QSignalMapper(self)
293
        self.__actionMapper.mapped[QObject].connect(self.__onTabActionToogled)
294
295
    def setTabButtonHeight(self, height):
296
        """
297
        Set the tab button height.
298
        """
299
        if self.__tabButtonHeight != height:
300
            self.__tabButtonHeight = height
301
            for page in self.__pages:
302
                page.button.setFixedHeight(height)
303
304
    def tabButtonHeight(self):
305
        """
306
        Return the tab button height.
307
        """
308
        return self.__tabButtonHeight
309
310
    def setTabIconSize(self, size):
311
        """
312
        Set the tab button icon size.
313
        """
314
        if self.__tabIconSize != size:
315
            self.__tabIconSize = size
316
            for page in self.__pages:
317
                page.button.setIconSize(size)
318
319
    def tabIconSize(self):
320
        """
321
        Return the tab icon size.
322
        """
323
        return self.__tabIconSize
324
325
    def tabButton(self, index):
326
        """
327
        Return the tab button at `index`
328
        """
329
        return self.__pages[index].button
330
331
    def tabAction(self, index):
332
        """
333
        Return open/close action for the tab at `index`.
334
        """
335
        return self.__pages[index].action
336
337
    def addItem(self, widget, text, icon=None, toolTip=None):
338
        """
339
        Append the `widget` in a new tab and return its index.
340
341
        Parameters
342
        ----------
343
        widget : :class:`QWidget`
344
            A widget to be inserted. The toolbox takes ownership
345
            of the widget.
346
347
        text : str
348
            Name/title of the new tab.
349
350
        icon : :class:`QIcon`, optional
351
            An icon for the tab button.
352
353
        toolTip : str, optional
354
            Tool tip for the tab button.
355
356
        """
357
        return self.insertItem(self.count(), widget, text, icon, toolTip)
358
359
    def insertItem(self, index, widget, text, icon=None, toolTip=None):
360
        """
361
        Insert the `widget` in a new tab at position `index`.
362
363
        See also
364
        --------
365
        ToolBox.addItem
366
367
        """
368
        button = self.createTabButton(widget, text, icon, toolTip)
369
370
        self.__contentsLayout.insertWidget(index * 2, button)
371
        self.__contentsLayout.insertWidget(index * 2 + 1, widget)
372
373
        widget.hide()
374
375
        page = _ToolBoxPage(index, widget, button.defaultAction(), button)
376
        self.__pages.insert(index, page)
377
378
        for i in range(index + 1, self.count()):
379
            self.__pages[i] = self.__pages[i]._replace(index=i)
380
381
        self.__updatePositions()
382
383
        # Show (open) the first tab.
384
        if self.count() == 1 and index == 0:
385
            page.action.trigger()
386
387
        self.__updateSelected()
388
389
        self.updateGeometry()
390
        return index
391
392
    def removeItem(self, index):
393
        """
394
        Remove the widget at `index`.
395
396
        .. note:: The widget hidden but is is not deleted.
397
398
        """
399
        self.__contentsLayout.takeAt(2 * index + 1)
400
        self.__contentsLayout.takeAt(2 * index)
401
        page = self.__pages.pop(index)
402
403
        # Update the page indexes
404
        for i in range(index, self.count()):
405
            self.__pages[i] = self.__pages[i]._replace(index=i)
406
407
        page.button.deleteLater()
408
409
        # Hide the widget and reparent to self
410
        # This follows QToolBox.removeItem
411
        page.widget.hide()
412
        page.widget.setParent(self)
413
414
        self.__updatePositions()
415
        self.__updateSelected()
416
417
        self.updateGeometry()
418
419
    def count(self):
420
        """
421
        Return the number of widgets inserted in the toolbox.
422
        """
423
        return len(self.__pages)
424
425
    def widget(self, index):
426
        """
427
        Return the widget at `index`.
428
        """
429
        return self.__pages[index].widget
430
431
    def createTabButton(self, widget, text, icon=None, toolTip=None):
0 ignored issues
show
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
432
        """
433
        Create the tab button for `widget`.
434
        """
435
        action = QAction(text, self)
436
        action.setCheckable(True)
437
438
        if icon:
439
            action.setIcon(icon)
440
441
        if toolTip:
442
            action.setToolTip(toolTip)
443
        self.__tabActionGroup.addAction(action)
444
        self.__actionMapper.setMapping(action, action)
445
        action.toggled.connect(self.__actionMapper.map)
446
447
        button = ToolBoxTabButton(self, objectName="toolbox-tab-button")
448
        button.setDefaultAction(action)
449
        button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
450
        button.setSizePolicy(QSizePolicy.Expanding,
451
                             QSizePolicy.Fixed)
452
453
        if self.__tabIconSize.isValid():
454
            button.setIconSize(self.__tabIconSize)
455
456
        if self.__tabButtonHeight > 0:
457
            button.setFixedHeight(self.__tabButtonHeight)
458
459
        return button
460
461
    def ensureWidgetVisible(self, child, xmargin=50, ymargin=50):
462
        """
463
        Scroll the contents so child widget instance is visible inside
464
        the viewport.
465
466
        """
467
        self.__scrollArea.ensureWidgetVisible(child, xmargin, ymargin)
468
469
    def sizeHint(self):
470
        hint = self.__contentsLayout.sizeHint()
471
472
        if self.count():
473
            # Compute max width of hidden widgets also.
474
            scroll = self.__scrollArea
475
            scroll_w = scroll.verticalScrollBar().sizeHint().width()
476
            frame_w = self.frameWidth() * 2 + scroll.frameWidth() * 2
477
            max_w = max([p.widget.sizeHint().width() for p in self.__pages])
478
            hint = QSize(max(max_w, hint.width()) + scroll_w + frame_w,
479
                         hint.height())
480
481
        return QSize(200, 200).expandedTo(hint)
482
483
    def __onTabActionToogled(self, action):
484
        page = find(self.__pages, action, key=attrgetter("action"))
485
        on = action.isChecked()
486
        page.widget.setVisible(on)
487
        index = page.index
488
489
        if index > 0:
490
            # Update the `previous` tab buttons style hints
491
            previous = self.__pages[index - 1].button
492
            flag = QStyleOptionToolBoxV2.NextIsSelected
493
            if on:
494
                previous.selected |= flag
495
            else:
496
                previous.selected &= ~flag
497
498
            previous.update()
499
500
        if index < self.count() - 1:
501
            next = self.__pages[index + 1].button
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in next.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
502
            flag = QStyleOptionToolBoxV2.PreviousIsSelected
503
            if on:
504
                next.selected |= flag
505
            else:
506
                next.selected &= ~flag
507
508
            next.update()
509
510
        self.tabToogled.emit(index, on)
511
512
        self.__contentsLayout.invalidate()
513
514
    def __updateSelected(self):
515
        """Update the tab buttons selected style flags.
516
        """
517
        if self.count() == 0:
518
            return
519
520
        opt = QStyleOptionToolBoxV2
521
522
        def update(button, next_sel, prev_sel):
523
            if next_sel:
524
                button.selected |= opt.NextIsSelected
525
            else:
526
                button.selected &= ~opt.NextIsSelected
527
528
            if prev_sel:
529
                button.selected |= opt.PreviousIsSelected
530
            else:
531
                button.selected &= ~ opt.PreviousIsSelected
532
533
            button.update()
534
535
        if self.count() == 1:
536
            update(self.__pages[0].button, False, False)
537
        elif self.count() >= 2:
538
            pages = self.__pages
539
            for i in range(1, self.count() - 1):
540
                update(pages[i].button,
541
                       pages[i + 1].action.isChecked(),
542
                       pages[i - 1].action.isChecked())
543
544
    def __updatePositions(self):
545
        """Update the tab buttons position style flags.
546
        """
547
        if self.count() == 0:
548
            return
549
        elif self.count() == 1:
550
            self.__pages[0].button.position = QStyleOptionToolBoxV2.OnlyOneTab
551
        else:
552
            self.__pages[0].button.position = QStyleOptionToolBoxV2.Beginning
553
            self.__pages[-1].button.position = QStyleOptionToolBoxV2.End
554
            for p in self.__pages[1:-1]:
555
                p.button.position = QStyleOptionToolBoxV2.Middle
556
557
        for p in self.__pages:
558
            p.button.update()
559
560
561
def identity(arg):
562
    return arg
563
564
565
def find(iterable, *what, **kwargs):
566
    """
567
    find(iterable, [what, [key=None, [predicate=operator.eq]]])
568
    """
569
    if what:
570
        what = what[0]
571
    key, predicate = kwargs.get("key", identity), kwargs.get("predicate", eq)
572
    for item in iterable:
573
        item_key = key(item)
574
        if predicate(item_key, what):
575
            return item
576
    else:
0 ignored issues
show
Bug introduced by
The else clause is not necessary as the loop does not contain a break statement.

If the loop cannot exit early through the use of break, the else part will always be executed. You can therefore just leave off the else.

Loading history...
577
        raise ValueError(what)
578