# piker: trading gear for hackers # Copyright (C) Tyler Goodlet (in stewardship for pikers) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . ''' Text entry "forms" widgets (mostly for configuration and UI user input). ''' from __future__ import annotations from contextlib import asynccontextmanager from functools import partial from math import floor from typing import ( Optional, Any, Callable, Awaitable ) import trio from PyQt5 import QtGui from PyQt5.QtCore import QSize, QModelIndex, Qt, QEvent from PyQt5.QtWidgets import ( QWidget, QLabel, QComboBox, QLineEdit, QHBoxLayout, QVBoxLayout, QFormLayout, QProgressBar, QSizePolicy, QStyledItemDelegate, QStyleOptionViewItem, ) from ._event import open_handlers from ._icons import mk_icons from ._style import hcolor, _font, _font_small, DpiAwareFont from ._label import FormatLabel class Edit(QLineEdit): def __init__( self, parent: QWidget, # parent_chart: QWidget, # noqa font: DpiAwareFont = _font, width_in_chars: int = None, ) -> None: # self.setContextMenuPolicy(Qt.CustomContextMenu) # self.customContextMenuRequested.connect(self.show_menu) # self.setStyleSheet(f"font: 18px") self.dpi_font = font # self.godwidget = parent_chart if width_in_chars: self._chars = int(width_in_chars) x_size_policy = QSizePolicy.Fixed else: # chart count which will be used to calculate # width of input field. self._chars: int = 6 # fit to surroundingn frame width x_size_policy = QSizePolicy.Expanding super().__init__(parent) # size it as we specify # https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum self.setSizePolicy( x_size_policy, QSizePolicy.Fixed, ) self.setFont(font.font) # witty bit of margin self.setTextMargins(2, 2, 2, 2) def sizeHint(self) -> QSize: """ Scale edit box to size of dpi aware font. """ psh = super().sizeHint() dpi_font = self.dpi_font psh.setHeight(dpi_font.px_size) # make space for ``._chars: int`` for of characters in view # TODO: somehow this math ain't right? chars_w_pxs = dpi_font.boundingRect('0'*self._chars).width() scale = round(dpi_font.scale()) psh.setWidth(int(chars_w_pxs * scale)) return psh def set_width_in_chars( self, chars: int, ) -> None: self._chars = chars self.sizeHint() self.update() def focus(self) -> None: self.selectAll() self.show() self.setFocus() class FontScaledDelegate(QStyledItemDelegate): ''' Super simple view delegate to render text in the same font size as the search widget. ''' def __init__( self, parent=None, font: DpiAwareFont = _font, ) -> None: super().__init__(parent) self.dpi_font = font def sizeHint( self, option: QStyleOptionViewItem, index: QModelIndex, ) -> QSize: # value = index.data() # br = self.dpi_font.boundingRect(value) # w, h = br.width(), br.height() parent = self.parent() if getattr(parent, '_max_item_size', None): return QSize(*self.parent()._max_item_size) else: return super().sizeHint(option, index) # NOTE: hack to display icons on RHS # TODO: is there a way to set this stype option once? # def paint(self, painter, option, index): # # display icons on RHS # # https://stackoverflow.com/a/39943629 # option.decorationPosition = QtGui.QStyleOptionViewItem.Right # option.decorationAlignment = Qt.AlignRight | Qt.AlignVCenter # QStyledItemDelegate.paint(self, painter, option, index) class Selection(QComboBox): def __init__( self, parent=None, ) -> None: self._items: dict[str, int] = {} super().__init__(parent=parent) self.setSizeAdjustPolicy(QComboBox.AdjustToContents) # make line edit expand to surrounding frame self.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Fixed, ) view = self.view() view.setUniformItemSizes(True) # TODO: this doesn't seem to work for the currently selected item? self.setItemDelegate(FontScaledDelegate(self)) self.resize() self._icons = mk_icons( self.style(), self.iconSize() ) def set_style( self, color: str, font_size: int, ) -> None: self.setStyleSheet( f"""QComboBox {{ color : {hcolor(color)}; font-size : {font_size}px; }} """ ) def resize( self, char: str = 'W', ) -> None: br = _font.boundingRect(str(char)) _, h = br.width(), int(br.height()) # TODO: something better then this monkey patch view = self.view() # XXX: see size policy settings of line edit # view._max_item_size = w, h self.setMinimumHeight(h) # at least one entry in view view.setMaximumHeight(6*h) # limit to 6 items max in view icon_size = round(h * 0.75) self.setIconSize(QSize(icon_size, icon_size)) def set_items( self, keys: list[str], ) -> None: ''' Write keys to the selection verbatim. All other items are cleared beforehand. ''' self.clear() self._items.clear() for i, key in enumerate(keys): strkey = str(key) self.insertItem(i, strkey) # store map of entry keys to row indexes self._items[strkey] = i # compute max item size so that the weird # "style item delegate" thing can then specify # that size on each item... keys.sort() self.resize(keys[-1]) def set_icon( self, key: str, icon_name: Optional[str], ) -> None: self.setItemIcon( self._items[key], self._icons[icon_name], ) def items(self) -> list[(str, int)]: return list(self._items.items()) # NOTE: in theory we can put icons on the RHS side with this hackery: # https://stackoverflow.com/a/64256969 # def showPopup(self): # print('show') # QComboBox.showPopup(self) # def hidePopup(self): # # self.setItemDelegate(FontScaledDelegate(self.parent())) # print('hide') # QComboBox.hidePopup(self) # slew of resources which helped get this where it is: # https://stackoverflow.com/questions/20648210/qcombobox-adjusttocontents-changing-height # https://stackoverflow.com/questions/3151798/how-do-i-set-the-qcombobox-width-to-fit-the-largest-item # https://stackoverflow.com/questions/6337589/qlistwidget-adjust-size-to-content#6370892 # https://stackoverflow.com/questions/25304267/qt-resize-of-qlistview # https://stackoverflow.com/questions/28227406/how-to-set-qlistview-rows-height-permanently class FieldsForm(QWidget): vbox: QVBoxLayout form: QFormLayout def __init__( self, parent=None, ) -> None: super().__init__(parent) # size it as we specify self.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding, ) # XXX: not sure why we have to create this here exactly # (instead of in the pane creation routine) but it's # here and is managed by downstream layout routines. # best guess is that you have to create layouts in order # of hierarchy in order for things to display correctly? # TODO: we may want to hand this *down* from some "pane manager" # thing eventually? self.vbox = QVBoxLayout(self) # self.vbox.setAlignment(Qt.AlignVCenter) self.vbox.setAlignment(Qt.AlignBottom) self.vbox.setContentsMargins(3, 6, 3, 6) self.vbox.setSpacing(0) # split layout for the (