Skip to content

Commit 19ab5df

Browse files
chore: responsive tab close button (#609)
1 parent cc30d71 commit 19ab5df

7 files changed

Lines changed: 117 additions & 41 deletions

File tree

apps/ttkode/ttkode/app/ttkode.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,23 @@ def isChanged(self) -> bool:
7575

7676
def getTabButtonStyle(self) -> dict:
7777
if self._changedStatus:
78-
return {'default':{'closeGlyph':' ● '}}
78+
return {
79+
'default':{
80+
'closeGlyph': {
81+
'default':' ● ',
82+
'hovered':' ● '
83+
}
84+
}
85+
}
7986
else:
80-
return {'default':{'closeGlyph':' □ '}}
87+
return {
88+
'default':{
89+
'closeGlyph': {
90+
'default':' ○ ',
91+
'hovered':' ○ '
92+
}
93+
}
94+
}
8195

8296
def _handleContentChanged(self) -> None:
8397
'''A signal is emitted when the file status change, marking it as modified or not'''

demo/showcase/tab.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def demoTab(root=None, border=True):
5353
tabWidget1.addMenu("ZZ", ttk.TTkK.RIGHT)
5454
tabWidget1.addMenu("KK", ttk.TTkK.RIGHT)
5555

56+
@ttk.pyTTkSlot(int)
57+
def _reportClose(num:int):
58+
tabWidget1.removeTab(num)
59+
60+
tabWidget1.tabCloseRequested.connect(_reportClose)
61+
5662
return tabWidget1
5763

5864
def main():

libs/pyTermTk/TermTk/TTkCore/ttk.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -320,20 +320,15 @@ def _mouse_event(self, mevt):
320320
# - Drag
321321
# - Release
322322
focusWidget = self._getFocusWidget()
323-
if ( focusWidget is not None and
324-
( mevt.evt == TTkK.Drag or
325-
mevt.evt == TTkK.Release ) and
326-
not TTkHelper.isDnD() ) :
327-
x,y = TTkHelper.absPos(focusWidget)
323+
pendingReleaseWidget = self._getPendingMouseReleaseWidget()
324+
325+
if ( pendingReleaseWidget is not None and
326+
mevt.evt in (TTkK.Drag,TTkK.Release) and
327+
not TTkHelper.isDnD() ) :
328+
x,y = TTkHelper.absPos(pendingReleaseWidget)
328329
nmevt = mevt.clone(pos=(mevt.x-x, mevt.y-y))
329-
focusWidget.mouseEvent(nmevt)
330+
pendingReleaseWidget.mouseEvent(nmevt)
330331
else:
331-
# Sometimes the release event is not retrieved
332-
if ( focusWidget and
333-
focusWidget._pendingMouseRelease and
334-
not TTkHelper.isDnD() ):
335-
focusWidget.mouseEvent(mevt.clone(evt=TTkK.Release))
336-
focusWidget._pendingMouseRelease = False
337332
# Adding this Crappy logic to handle a corner case in the drop routine
338333
# where the mouse is leaving any widget able to handle the drop event
339334
if not self.mouseEvent(mevt):
@@ -347,6 +342,7 @@ def _mouse_event(self, mevt):
347342
# Clean the Drag and Drop in case of mouse release
348343
if mevt.evt == TTkK.Release:
349344
TTkHelper.dndEnd()
345+
self._setPendingMouseReleaseWidget(None)
350346

351347
def _time_event(self):
352348
# Event.{wait and clear} should be atomic,

libs/pyTermTk/TermTk/TTkWidgets/container.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,16 @@ def __init__(self, *,
155155
self._layout.setParent(self)
156156
self.update(updateLayout=True)
157157

158+
def _getPendingMouseReleaseWidget(self) -> Optional[TTkWidget]:
159+
if (_pw:=self.parentWidget()):
160+
return _pw._getPendingMouseReleaseWidget()
161+
return None
162+
163+
def _setPendingMouseReleaseWidget(self, widget:Optional[TTkWidget]) -> None:
164+
if not (_pw:=self.parentWidget()):
165+
return
166+
_pw._setPendingMouseReleaseWidget(widget)
167+
158168
def _getFocusWidget(self) -> Optional[TTkWidget]:
159169
if (_pw:=self.parentWidget()):
160170
return _pw._getFocusWidget()

libs/pyTermTk/TermTk/TTkWidgets/rootcontainer.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,28 @@ class _TTkRootContainer(TTkContainer):
7171
consumes the event, ensuring focus loops back to the first/last focusable widget.
7272
'''
7373
__slots__ = (
74+
'_pendingMouseReleaseWidget',
7475
'_focusWidget',
7576
'_overlay')
7677

78+
_pendingMouseReleaseWidget:Optional[TTkWidget]
7779
_focusWidget:Optional[TTkWidget]
7880
_overlay:List[_TTkOverlay]
7981

8082
def __init__(self, **kwargs) -> None:
83+
self._pendingMouseReleaseWidget = None
8184
self._focusWidget = None
8285
self._overlay = []
8386
super().__init__(**kwargs)
8487

88+
def _getPendingMouseReleaseWidget(self) -> Optional[TTkWidget]:
89+
return self._pendingMouseReleaseWidget
90+
91+
def _setPendingMouseReleaseWidget(self, widget:Optional[TTkWidget]) -> None:
92+
if self._pendingMouseReleaseWidget is widget:
93+
return
94+
self._pendingMouseReleaseWidget = widget
95+
8596
def _getFocusWidget(self) -> Optional[TTkWidget]:
8697
'''
8798
Returns the currently focused widget.

libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ def _popButton(self, index:int) -> Optional[TTkTabButton]:
172172
'default': {'color': TTkColor.fgbg("#dddd88","#000044"),
173173
'bgColor': TTkColor.fgbg("#000000","#8888aa"),
174174
'borderColor': TTkColor.RST,
175+
'closeColor': {
176+
'default' : TTkColor.CYAN,
177+
'hovered' : TTkColor.YELLOW
178+
},
175179
'borderHighlightColors': {
176180
'main' : TTkColor.fg('#00FFFF'),
177181
'fade' : TTkColor.fg('#88FF88'),
@@ -262,10 +266,6 @@ def __init__(self, *,
262266
self._tabStatus = tabStatus
263267
super().__init__(**kwargs)
264268

265-
def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool:
266-
self.tcbClicked.emit(self)
267-
return True
268-
269269
def keyEvent(self, evt:TTkKeyEvent) -> bool:
270270
if ( evt.type == TTkK.Character and evt.key==" " ) or \
271271
( evt.type == TTkK.SpecialKey and evt.key == TTkK.Key_Enter ):
@@ -280,17 +280,31 @@ class TTkTabButton(_TTkTabColorButton):
280280
classStyle = (
281281
_TTkTabColorButton.classStyle |
282282
{ 'default': _TTkTabColorButton.classStyle['default'] |
283-
{'closeGlyph':' □ '} ,
283+
{
284+
'closeGlyph': {
285+
'default':' ○ ',
286+
'hovered':' ○ '
287+
}
288+
} ,
284289
'hover': _TTkTabColorButton.classStyle['hover'] |
285-
{'closeGlyph':' x '} } )
290+
{
291+
'closeGlyph':{
292+
'default':' □ ',
293+
'hovered':' ▣ '
294+
}
295+
}
296+
}
297+
)
286298

287299
'''TTkTabButton'''
288300
__slots__ = (
289-
'_data','_sideEnd', '_buttonStatus', '_closable',
301+
'_data','_sideEnd', '_buttonStatus', '_closable', '_closeHovered',
290302
'closeClicked', '_closeButtonPressed', '_text')
291303

304+
_closeHovered:bool
305+
292306
def __init__(self, *,
293-
text:TTkString='',
307+
text:TTkStringType='',
294308
data:object=None,
295309
closable:bool=False,
296310
**kwargs) -> None:
@@ -299,6 +313,7 @@ def __init__(self, *,
299313
self._buttonStatus = TTkK.Unchecked
300314
self._data = data
301315
self._closable = closable
316+
self._closeHovered = False
302317
self.closeClicked = pyTTkSignal()
303318
super().__init__(**kwargs)
304319
self._closeButtonPressed = False
@@ -308,7 +323,7 @@ def _resetSize(self) -> None:
308323
style = self.currentStyle()
309324
size = self.text().termWidth() + 2
310325
if self._closable:
311-
size += len(style['closeGlyph'])
326+
size += len(style['closeGlyph']['default'])
312327
self.resize(size, self._tabStatus.barType.vSize())
313328
self.setMinimumSize(size, self._tabStatus.barType.vSize())
314329
self.setMaximumSize(size, self._tabStatus.barType.vSize())
@@ -344,25 +359,42 @@ def mousePressEvent(self, evt:TTkMouseEvent) -> bool:
344359
x,y = evt.x,evt.y
345360
w,h = self.size()
346361
self._closeButtonPressed = False
347-
if self._closable and evt.key == TTkK.MidButton:
348-
self.closeClicked.emit()
349-
return True
350362
offY = self._tabStatus.barType.offY()
351363
if self._closable and y == offY and w-4<=x<w-1:
352364
self._closeButtonPressed = True
365+
self.update()
353366
return True
354-
return super().mouseReleaseEvent(evt)
367+
self.tcbClicked.emit(self)
368+
return True
355369

356370
def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool:
357371
x,y = evt.x,evt.y
358372
w,h = self.size()
359373
offY = self._tabStatus.barType.offY()
360-
if self._closable and y == offY and w-4<=x<w-1 and self._closeButtonPressed:
361-
self._closeButtonPressed = False
374+
if self._closable and evt.key == TTkK.MidButton:
375+
self.closeClicked.emit()
376+
elif self._closable and y == offY and w-4<=x<w-1 and self._closeButtonPressed:
362377
self.closeClicked.emit()
363-
return False
364378
self._closeButtonPressed = False
365-
return False
379+
return True
380+
381+
def leaveEvent(self, evt):
382+
self._closeHovered = False
383+
self.update()
384+
return super().leaveEvent(evt)
385+
386+
def mouseMoveEvent(self, evt):
387+
x,y = evt.x,evt.y
388+
w,h = self.size()
389+
offY = self._tabStatus.barType.offY()
390+
if self._closable and y == offY and w-4<=x<w-1:
391+
_new_closeHovered = True
392+
else:
393+
_new_closeHovered = False
394+
if self._closeHovered != _new_closeHovered:
395+
self._closeHovered = _new_closeHovered
396+
self.update()
397+
return True
366398

367399
def mouseDragEvent(self, evt:TTkMouseEvent) -> bool:
368400
drag = TTkDrag()
@@ -390,7 +422,7 @@ def paintEvent(self, canvas: TTkCanvas) -> None:
390422

391423
borderColor:TTkColor = style['borderColor']
392424
textColor:TTkColor = style['color']
393-
borderHighlightColors:TTkColor = style['borderHighlightColors']
425+
borderHighlightColors:Dict[str, TTkColor] = style['borderHighlightColors']
394426

395427
w,h = self.size()
396428
offY = self._tabStatus.barType.offY()
@@ -543,9 +575,14 @@ def paintEvent(self, canvas: TTkCanvas) -> None:
543575
canvas.drawText(pos=(1,offY), text=self.text(), color=textColor)
544576

545577
if self._closable:
546-
closeGlyph = style['closeGlyph']
578+
if self._closeHovered:
579+
colorCloseHovered = textColor+style['closeColor']['hovered']
580+
closeGlyph = style['closeGlyph']['hovered']
581+
else:
582+
colorCloseHovered = textColor+style['closeColor']['default']
583+
closeGlyph = style['closeGlyph']['default']
547584
closeOff = len(closeGlyph)
548-
canvas.drawText(pos=(w-closeOff-1,offY), text=closeGlyph, color=textColor)
585+
canvas.drawText(pos=(w-closeOff-1,offY), text=closeGlyph, color=colorCloseHovered)
549586

550587
class _TTkTabMenuButton(TTkMenuBarButton):
551588
def paintEvent(self, canvas: TTkCanvas) -> None:
@@ -580,7 +617,8 @@ def setSideEnd(self, sideEnd: int) -> None:
580617
# This is a hack to force the action aftet the keypress
581618
# And not key release as normally happen to the button
582619
def mousePressEvent(self, evt:TTkMouseEvent) -> bool:
583-
return super().mouseReleaseEvent(evt)
620+
self.tcbClicked.emit(self)
621+
return True
584622
def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool:
585623
return False
586624
def mouseTapEvent(self, evt:TTkMouseEvent) -> bool:

libs/pyTermTk/TermTk/TTkWidgets/widget.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ class TTkWidget(TMouseEvents, TKeyEvents, TDragEvents):
113113
'_focus_policy',
114114
'_canvas', '_widgetItem',
115115
'_visible',
116-
'_pendingMouseRelease',
117116
'_enabled',
118117
'_style', '_currentStyle',
119118
'_toolTip',
@@ -136,7 +135,6 @@ class TTkWidget(TMouseEvents, TKeyEvents, TDragEvents):
136135
_canvas:TTkCanvas
137136
_widgetItem:TTkWidgetItem
138137
_visible:bool
139-
_pendingMouseRelease:bool
140138
_enabled:bool
141139
_style:Dict
142140
_currentStyle:Dict
@@ -230,7 +228,6 @@ def __init__(
230228
self._name = name if name else self.__class__.__name__
231229
self._parent = parent
232230

233-
self._pendingMouseRelease = False
234231

235232
self._x, self._y = pos if pos else (x,y)
236233
self._width, self._height = size if size else (width,height)
@@ -510,7 +507,6 @@ def mouseEvent(self, evt:TTkMouseEvent) -> bool:
510507
TTkHelper.toolTipClose()
511508

512509
if evt.evt == TTkK.Release:
513-
self._pendingMouseRelease = False
514510
self._processStyleEvent(TTkWidget._S_NONE)
515511
if self.mouseReleaseEvent(evt):
516512
return True
@@ -525,13 +521,12 @@ def mouseEvent(self, evt:TTkMouseEvent) -> bool:
525521
w.setFocus()
526522
w.raiseWidget()
527523
if evt.tap == 2 and self.mouseDoubleClickEvent(evt):
528-
#self._pendingMouseRelease = True
529524
return True
530525
if evt.tap > 1 and self.mouseTapEvent(evt):
531526
return True
532527
if evt.tap == 1 and self.mousePressEvent(evt):
533528
# TTkLog.debug(f"Click {self._name}")
534-
self._pendingMouseRelease = True
529+
self._setPendingMouseRelease()
535530
return True
536531

537532
if evt.key == TTkK.Wheel:
@@ -705,6 +700,12 @@ def paintEvent(self, canvas:TTkCanvas) -> None:
705700
if updateParent and self._parent is not None:
706701
self._parent.update(updateLayout=True)
707702

703+
def _setPendingMouseRelease(self) -> None:
704+
'''set the Pending Mouse Release Widget'''
705+
if not (_p:=self._parent):
706+
return
707+
_p._setPendingMouseReleaseWidget(self)
708+
708709
@pyTTkSlot()
709710
def setFocus(self) -> None:
710711
'''Focus the widget'''

0 commit comments

Comments
 (0)