Compare commits
No commits in common. "cd15f2ae765f5c958f78810a8bf98574bbd302d6" and "db77d7ab2942aa182048c5f7b34b3090ba004983" have entirely different histories.
cd15f2ae76
...
db77d7ab29
|
|
@ -103,4 +103,3 @@ ENV/
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
**/.DS_Store
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,9 @@ tsdb.host = 'localhost'
|
||||||
tsdb.grpc_port = 5995
|
tsdb.grpc_port = 5995
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
# set custom font + size which will scale entire UI~
|
# set custom font + size which will scale entire UI
|
||||||
# font_size = 16
|
# font_size = 16
|
||||||
# font_size = 32
|
|
||||||
# font_name = 'Monospaced'
|
# font_name = 'Monospaced'
|
||||||
|
|
||||||
# colorscheme = 'default' # UNUSED
|
# colorscheme = 'default' # UNUSED
|
||||||
# graphics.update_throttle = 120 # Hz #PENDING TODO
|
# graphics.update_throttle = 60 # Hz # TODO
|
||||||
|
|
||||||
|
|
|
||||||
20
piker.sh
20
piker.sh
|
|
@ -1,20 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# macOS wrapper for piker to handle missing XDG_RUNTIME_DIR
|
|
||||||
|
|
||||||
# Set up runtime directory for macOS if not already set
|
|
||||||
if [ -z "$XDG_RUNTIME_DIR" ]; then
|
|
||||||
# Use macOS standard temp directory with user-specific subdirectory
|
|
||||||
export XDG_RUNTIME_DIR="/tmp/piker-runtime-$(id -u)"
|
|
||||||
|
|
||||||
# Create the directory if it doesn't exist
|
|
||||||
if [ ! -d "$XDG_RUNTIME_DIR" ]; then
|
|
||||||
mkdir -p "$XDG_RUNTIME_DIR"
|
|
||||||
# Set proper permissions (only user can access)
|
|
||||||
chmod 700 "$XDG_RUNTIME_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Set XDG_RUNTIME_DIR to: $XDG_RUNTIME_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run piker with all passed arguments
|
|
||||||
exec uv run piker "$@"
|
|
||||||
|
|
@ -183,8 +183,8 @@ def pikerd(
|
||||||
registry_addrs=regaddrs,
|
registry_addrs=regaddrs,
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
debug_mode=pdb,
|
debug_mode=pdb,
|
||||||
# enable_transports=['uds'],
|
enable_transports=['uds'],
|
||||||
enable_transports=['tcp'],
|
# enable_transports=['tcp'],
|
||||||
) as service_mngr,
|
) as service_mngr,
|
||||||
):
|
):
|
||||||
assert service_mngr
|
assert service_mngr
|
||||||
|
|
|
||||||
|
|
@ -92,15 +92,6 @@ class SymbologyCache(Struct):
|
||||||
|
|
||||||
def write_config(self) -> None:
|
def write_config(self) -> None:
|
||||||
|
|
||||||
def clean_dict_for_toml(d):
|
|
||||||
'''Remove None values from dict recursively for TOML serialization'''
|
|
||||||
if isinstance(d, dict):
|
|
||||||
return {k: clean_dict_for_toml(v) for k, v in d.items() if v is not None}
|
|
||||||
elif isinstance(d, list):
|
|
||||||
return [clean_dict_for_toml(item) for item in d if item is not None]
|
|
||||||
else:
|
|
||||||
return d
|
|
||||||
|
|
||||||
# put the backend's pair-struct type ref at the top
|
# put the backend's pair-struct type ref at the top
|
||||||
# of file if possible.
|
# of file if possible.
|
||||||
cachedict: dict[str, Any] = {
|
cachedict: dict[str, Any] = {
|
||||||
|
|
@ -121,9 +112,7 @@ class SymbologyCache(Struct):
|
||||||
|
|
||||||
dct = cachedict[key] = {}
|
dct = cachedict[key] = {}
|
||||||
for key, struct in table.items():
|
for key, struct in table.items():
|
||||||
raw_dict = struct.to_dict(include_non_members=False)
|
dct[key] = struct.to_dict(include_non_members=False)
|
||||||
# Clean None values for TOML compatibility
|
|
||||||
dct[key] = clean_dict_for_toml(raw_dict)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with self.fp.open(mode='wb') as fp:
|
with self.fp.open(mode='wb') as fp:
|
||||||
|
|
|
||||||
|
|
@ -200,13 +200,9 @@ def maybe_mk_fsp_shm(
|
||||||
)
|
)
|
||||||
|
|
||||||
# (attempt to) uniquely key the fsp shm buffers
|
# (attempt to) uniquely key the fsp shm buffers
|
||||||
# Use hash for macOS compatibility (31 char limit)
|
|
||||||
import hashlib
|
|
||||||
actor_name, uuid = tractor.current_actor().uid
|
actor_name, uuid = tractor.current_actor().uid
|
||||||
# Create short hash of sym and target name
|
uuid_snip: str = uuid[:16]
|
||||||
content = f'{sym}.{target.name}'
|
key: str = f'piker.{actor_name}[{uuid_snip}].{sym}.{target.name}'
|
||||||
content_hash = hashlib.md5(content.encode()).hexdigest()[:8]
|
|
||||||
key: str = f'{uuid[:8]}_{content_hash}.fsp'
|
|
||||||
|
|
||||||
shm, opened = maybe_open_shm_array(
|
shm, opened = maybe_open_shm_array(
|
||||||
key,
|
key,
|
||||||
|
|
|
||||||
|
|
@ -1257,17 +1257,13 @@ async def manage_history(
|
||||||
service: str = name.rstrip(f'.{mod.name}')
|
service: str = name.rstrip(f'.{mod.name}')
|
||||||
fqme: str = mkt.get_fqme(delim_char='')
|
fqme: str = mkt.get_fqme(delim_char='')
|
||||||
|
|
||||||
# Create a short hash of the fqme for macOS compatibility
|
|
||||||
import hashlib
|
|
||||||
fqme_hash = hashlib.md5(fqme.encode()).hexdigest()[:8]
|
|
||||||
|
|
||||||
# (maybe) allocate shm array for this broker/symbol which will
|
# (maybe) allocate shm array for this broker/symbol which will
|
||||||
# be used for fast near-term history capture and processing.
|
# be used for fast near-term history capture and processing.
|
||||||
hist_shm, opened = maybe_open_shm_array(
|
hist_shm, opened = maybe_open_shm_array(
|
||||||
size=_default_hist_size,
|
size=_default_hist_size,
|
||||||
append_start_index=_hist_buffer_start,
|
append_start_index=_hist_buffer_start,
|
||||||
|
|
||||||
key=f'{uuid[:8]}_{fqme_hash}.h',
|
key=f'piker.{service}[{uuid[:16]}].{fqme}.hist',
|
||||||
|
|
||||||
# use any broker defined ohlc dtype:
|
# use any broker defined ohlc dtype:
|
||||||
dtype=getattr(mod, '_ohlc_dtype', def_iohlcv_fields),
|
dtype=getattr(mod, '_ohlc_dtype', def_iohlcv_fields),
|
||||||
|
|
@ -1286,7 +1282,7 @@ async def manage_history(
|
||||||
rt_shm, opened = maybe_open_shm_array(
|
rt_shm, opened = maybe_open_shm_array(
|
||||||
size=_default_rt_size,
|
size=_default_rt_size,
|
||||||
append_start_index=_rt_buffer_start,
|
append_start_index=_rt_buffer_start,
|
||||||
key=f'{uuid[:8]}_{fqme_hash}.r',
|
key=f'piker.{service}[{uuid[:16]}].{fqme}.rt',
|
||||||
|
|
||||||
# use any broker defined ohlc dtype:
|
# use any broker defined ohlc dtype:
|
||||||
dtype=getattr(mod, '_ohlc_dtype', def_iohlcv_fields),
|
dtype=getattr(mod, '_ohlc_dtype', def_iohlcv_fields),
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,6 @@ class Axis(pg.AxisItem):
|
||||||
self.pi = plotitem
|
self.pi = plotitem
|
||||||
self._dpi_font = _font
|
self._dpi_font = _font
|
||||||
|
|
||||||
# store for later recalculation on zoom
|
|
||||||
self._typical_max_str = typical_max_str
|
|
||||||
|
|
||||||
self.setTickFont(_font.font)
|
self.setTickFont(_font.font)
|
||||||
font_size = self._dpi_font.font.pixelSize()
|
font_size = self._dpi_font.font.pixelSize()
|
||||||
|
|
||||||
|
|
@ -159,41 +156,6 @@ class Axis(pg.AxisItem):
|
||||||
def size_to_values(self) -> None:
|
def size_to_values(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_fonts(self, font: DpiAwareFont) -> None:
|
|
||||||
'''Update font and recalculate axis sizing after zoom change.'''
|
|
||||||
# IMPORTANT: tell Qt we're about to change geometry
|
|
||||||
self.prepareGeometryChange()
|
|
||||||
|
|
||||||
self._dpi_font = font
|
|
||||||
self.setTickFont(font.font)
|
|
||||||
font_size = font.font.pixelSize()
|
|
||||||
|
|
||||||
# recalculate text offset based on new font size
|
|
||||||
text_offset = None
|
|
||||||
if self.orientation in ('bottom',):
|
|
||||||
text_offset = floor(0.25 * font_size)
|
|
||||||
elif self.orientation in ('left', 'right'):
|
|
||||||
text_offset = floor(font_size / 2)
|
|
||||||
|
|
||||||
if text_offset:
|
|
||||||
self.setStyle(tickTextOffset=text_offset)
|
|
||||||
|
|
||||||
# recalculate bounding rect with new font
|
|
||||||
# Note: typical_max_str should be stored from init
|
|
||||||
if not hasattr(self, '_typical_max_str'):
|
|
||||||
self._typical_max_str = '100 000.000 ' # fallback default
|
|
||||||
self.typical_br = font._qfm.boundingRect(self._typical_max_str)
|
|
||||||
|
|
||||||
# Update PyQtGraph's internal text size tracking
|
|
||||||
# This is critical - PyQtGraph uses these internally for auto-expand
|
|
||||||
if self.orientation in ['left', 'right']:
|
|
||||||
self.textWidth = self.typical_br.width()
|
|
||||||
else:
|
|
||||||
self.textHeight = self.typical_br.height()
|
|
||||||
|
|
||||||
# resize axis to fit new font - this triggers PyQtGraph's auto-expand
|
|
||||||
self.size_to_values()
|
|
||||||
|
|
||||||
def txt_offsets(self) -> tuple[int, int]:
|
def txt_offsets(self) -> tuple[int, int]:
|
||||||
return tuple(self.style['tickTextOffset'])
|
return tuple(self.style['tickTextOffset'])
|
||||||
|
|
||||||
|
|
@ -294,14 +256,7 @@ class PriceAxis(Axis):
|
||||||
self._min_tick = size
|
self._min_tick = size
|
||||||
|
|
||||||
def size_to_values(self) -> None:
|
def size_to_values(self) -> None:
|
||||||
# Call PyQtGraph's internal width update mechanism
|
self.setWidth(self.typical_br.width())
|
||||||
# This respects autoExpandTextSpace and updates min/max constraints
|
|
||||||
self._updateWidth()
|
|
||||||
# tell Qt our preferred size changed so layout recalculates
|
|
||||||
self.updateGeometry()
|
|
||||||
# force parent plot item to recalculate its layout
|
|
||||||
if self.pi and hasattr(self.pi, 'updateGeometry'):
|
|
||||||
self.pi.updateGeometry()
|
|
||||||
|
|
||||||
# XXX: drop for now since it just eats up h space
|
# XXX: drop for now since it just eats up h space
|
||||||
|
|
||||||
|
|
@ -345,14 +300,7 @@ class DynamicDateAxis(Axis):
|
||||||
}
|
}
|
||||||
|
|
||||||
def size_to_values(self) -> None:
|
def size_to_values(self) -> None:
|
||||||
# Call PyQtGraph's internal height update mechanism
|
self.setHeight(self.typical_br.height() + 1)
|
||||||
# This respects autoExpandTextSpace and updates min/max constraints
|
|
||||||
self._updateHeight()
|
|
||||||
# tell Qt our preferred size changed so layout recalculates
|
|
||||||
self.updateGeometry()
|
|
||||||
# force parent plot item to recalculate its layout
|
|
||||||
if self.pi and hasattr(self.pi, 'updateGeometry'):
|
|
||||||
self.pi.updateGeometry()
|
|
||||||
|
|
||||||
def _indexes_to_timestrs(
|
def _indexes_to_timestrs(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -212,9 +212,7 @@ async def increment_history_view(
|
||||||
hist_chart: ChartPlotWidget = ds.hist_chart
|
hist_chart: ChartPlotWidget = ds.hist_chart
|
||||||
hist_viz: Viz = ds.hist_viz
|
hist_viz: Viz = ds.hist_viz
|
||||||
# viz: Viz = ds.viz
|
# viz: Viz = ds.viz
|
||||||
# NOTE: Changed for macOS compatibility with shortened shm names
|
assert 'hist' in hist_viz.shm.token['shm_name']
|
||||||
# assert 'hist' in hist_viz.shm.token['shm_name']
|
|
||||||
assert hist_viz.shm.token['shm_name'].endswith('.h')
|
|
||||||
# name: str = hist_viz.name
|
# name: str = hist_viz.name
|
||||||
|
|
||||||
# TODO: seems this is more reliable at keeping the slow
|
# TODO: seems this is more reliable at keeping the slow
|
||||||
|
|
|
||||||
|
|
@ -199,9 +199,6 @@ def run_qtractor(
|
||||||
if is_windows:
|
if is_windows:
|
||||||
window.configure_to_desktop()
|
window.configure_to_desktop()
|
||||||
|
|
||||||
# install global keyboard shortcuts for UI zoom
|
|
||||||
window.install_global_zoom_filter()
|
|
||||||
|
|
||||||
# actually render to screen
|
# actually render to screen
|
||||||
window.show()
|
window.show()
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
|
||||||
|
|
@ -124,13 +124,6 @@ class Edit(QLineEdit):
|
||||||
self.sizeHint()
|
self.sizeHint()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def update_fonts(self, font: DpiAwareFont) -> None:
|
|
||||||
'''Update font and recalculate widget size.'''
|
|
||||||
self.dpi_font = font
|
|
||||||
self.setFont(font.font)
|
|
||||||
# tell Qt our size hint changed so it recalculates layout
|
|
||||||
self.updateGeometry()
|
|
||||||
|
|
||||||
def focus(self) -> None:
|
def focus(self) -> None:
|
||||||
self.selectAll()
|
self.selectAll()
|
||||||
self.show()
|
self.show()
|
||||||
|
|
@ -248,14 +241,6 @@ class Selection(QComboBox):
|
||||||
icon_size = round(h * 0.75)
|
icon_size = round(h * 0.75)
|
||||||
self.setIconSize(QSize(icon_size, icon_size))
|
self.setIconSize(QSize(icon_size, icon_size))
|
||||||
|
|
||||||
def update_fonts(self, font: DpiAwareFont) -> None:
|
|
||||||
'''Update font and recalculate widget size.'''
|
|
||||||
self.setFont(font.font)
|
|
||||||
# recalculate heights with new font
|
|
||||||
self.resize()
|
|
||||||
# tell Qt our size hint changed so it recalculates layout
|
|
||||||
self.updateGeometry()
|
|
||||||
|
|
||||||
def set_items(
|
def set_items(
|
||||||
self,
|
self,
|
||||||
keys: list[str],
|
keys: list[str],
|
||||||
|
|
@ -446,39 +431,6 @@ class FieldsForm(QWidget):
|
||||||
self.fields[key] = select
|
self.fields[key] = select
|
||||||
return select
|
return select
|
||||||
|
|
||||||
def update_fonts(self) -> None:
|
|
||||||
'''Update font sizes after zoom change.'''
|
|
||||||
from ._style import _font, _font_small
|
|
||||||
|
|
||||||
# update stored font size
|
|
||||||
self._font_size = _font_small.px_size - 2
|
|
||||||
|
|
||||||
# update all labels
|
|
||||||
for name, label in self.labels.items():
|
|
||||||
if hasattr(label, 'update_font'):
|
|
||||||
label.update_font(_font.font, self._font_size - 1)
|
|
||||||
|
|
||||||
# update all fields (edits, selects)
|
|
||||||
for key, field in self.fields.items():
|
|
||||||
# first check for our custom update_fonts method (Edit, Selection)
|
|
||||||
if hasattr(field, 'update_fonts'):
|
|
||||||
field.update_fonts(_font)
|
|
||||||
# then handle stylesheet updates for those without custom methods
|
|
||||||
elif hasattr(field, 'setStyleSheet'):
|
|
||||||
# regenerate stylesheet with new font size
|
|
||||||
field.setStyleSheet(
|
|
||||||
f"""QLineEdit {{
|
|
||||||
color : {hcolor('gunmetal')};
|
|
||||||
font-size : {self._font_size}px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
field.setFont(_font.font)
|
|
||||||
|
|
||||||
# for Selection widgets that need style updates
|
|
||||||
if hasattr(field, 'set_style'):
|
|
||||||
field.set_style(color='gunmetal', font_size=self._font_size)
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_field_input(
|
async def handle_field_input(
|
||||||
|
|
||||||
|
|
@ -681,37 +633,6 @@ class FillStatusBar(QProgressBar):
|
||||||
self.setRange(0, int(slots))
|
self.setRange(0, int(slots))
|
||||||
self.setValue(value)
|
self.setValue(value)
|
||||||
|
|
||||||
def update_fonts(self, font_size: int) -> None:
|
|
||||||
'''Update font size after zoom change.'''
|
|
||||||
from ._style import _font_small
|
|
||||||
|
|
||||||
self.font_size = font_size
|
|
||||||
# regenerate stylesheet with new font size
|
|
||||||
self.setStyleSheet(
|
|
||||||
f"""
|
|
||||||
QProgressBar {{
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
font-size : {self.font_size - 2}px;
|
|
||||||
|
|
||||||
background-color: {hcolor('papas_special')};
|
|
||||||
color : {hcolor('papas_special')};
|
|
||||||
|
|
||||||
border: {self.border_px}px solid {hcolor('default_light')};
|
|
||||||
border-radius: 2px;
|
|
||||||
}}
|
|
||||||
QProgressBar::chunk {{
|
|
||||||
|
|
||||||
background-color: {hcolor('default_spotlight')};
|
|
||||||
color: {hcolor('bracket')};
|
|
||||||
|
|
||||||
border-radius: 2px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
self.setFont(_font_small.font)
|
|
||||||
|
|
||||||
|
|
||||||
def mk_fill_status_bar(
|
def mk_fill_status_bar(
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -334,19 +334,3 @@ class FormatLabel(QLabel):
|
||||||
out = self.fmt_str.format(**fields)
|
out = self.fmt_str.format(**fields)
|
||||||
self.setText(out)
|
self.setText(out)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def update_font(
|
|
||||||
self,
|
|
||||||
font: QtGui.QFont,
|
|
||||||
font_size: int,
|
|
||||||
font_color: str = 'default_lightest',
|
|
||||||
) -> None:
|
|
||||||
'''Update font after zoom change.'''
|
|
||||||
self.setStyleSheet(
|
|
||||||
f"""QLabel {{
|
|
||||||
color : {hcolor(font_color)};
|
|
||||||
font-size : {font_size}px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
self.setFont(font)
|
|
||||||
|
|
|
||||||
|
|
@ -178,26 +178,6 @@ class SettingsPane:
|
||||||
# encompasing high level namespace
|
# encompasing high level namespace
|
||||||
order_mode: OrderMode | None = None # typing: ignore # noqa
|
order_mode: OrderMode | None = None # typing: ignore # noqa
|
||||||
|
|
||||||
def update_fonts(self) -> None:
|
|
||||||
'''Update font sizes after zoom change.'''
|
|
||||||
from ._style import _font_small
|
|
||||||
|
|
||||||
# update form fields
|
|
||||||
if self.form and hasattr(self.form, 'update_fonts'):
|
|
||||||
self.form.update_fonts()
|
|
||||||
|
|
||||||
# update fill status bar
|
|
||||||
if self.fill_bar and hasattr(self.fill_bar, 'update_fonts'):
|
|
||||||
self.fill_bar.update_fonts(_font_small.px_size)
|
|
||||||
|
|
||||||
# update labels with new fonts
|
|
||||||
if self.step_label:
|
|
||||||
self.step_label.setFont(_font_small.font)
|
|
||||||
if self.pnl_label:
|
|
||||||
self.pnl_label.setFont(_font_small.font)
|
|
||||||
if self.limit_label:
|
|
||||||
self.limit_label.setFont(_font_small.font)
|
|
||||||
|
|
||||||
def set_accounts(
|
def set_accounts(
|
||||||
self,
|
self,
|
||||||
names: list[str],
|
names: list[str],
|
||||||
|
|
|
||||||
|
|
@ -174,12 +174,6 @@ class CompleterView(QTreeView):
|
||||||
|
|
||||||
self.setStyleSheet(f"font: {size}px")
|
self.setStyleSheet(f"font: {size}px")
|
||||||
|
|
||||||
def update_fonts(self) -> None:
|
|
||||||
'''Update font sizes after zoom change.'''
|
|
||||||
self.set_font_size(_font.px_size)
|
|
||||||
self.setIndentation(_font.px_size)
|
|
||||||
self.setFont(_font.font)
|
|
||||||
|
|
||||||
def resize_to_results(
|
def resize_to_results(
|
||||||
self,
|
self,
|
||||||
w: float | None = 0,
|
w: float | None = 0,
|
||||||
|
|
@ -636,27 +630,6 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
| align_flag.AlignLeft,
|
| align_flag.AlignLeft,
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_fonts(self) -> None:
|
|
||||||
'''Update font sizes after zoom change.'''
|
|
||||||
# regenerate label stylesheet with new font size
|
|
||||||
self.label.setStyleSheet(
|
|
||||||
f"""QLabel {{
|
|
||||||
color : {hcolor('default_lightest')};
|
|
||||||
font-size : {_font.px_size - 2}px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
self.label.setFont(_font.font)
|
|
||||||
|
|
||||||
# update search bar and view fonts
|
|
||||||
if hasattr(self.bar, 'update_fonts'):
|
|
||||||
self.bar.update_fonts(_font)
|
|
||||||
elif hasattr(self.bar, 'setFont'):
|
|
||||||
self.bar.setFont(_font.font)
|
|
||||||
|
|
||||||
if hasattr(self.view, 'update_fonts'):
|
|
||||||
self.view.update_fonts()
|
|
||||||
|
|
||||||
def focus(self) -> None:
|
def focus(self) -> None:
|
||||||
self.show()
|
self.show()
|
||||||
self.bar.focus()
|
self.bar.focus()
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ class DpiAwareFont:
|
||||||
self._screen = None
|
self._screen = None
|
||||||
|
|
||||||
def _set_qfont_px_size(self, px_size: int) -> None:
|
def _set_qfont_px_size(self, px_size: int) -> None:
|
||||||
self._qfont.setPixelSize(int(px_size))
|
self._qfont.setPixelSize(px_size)
|
||||||
self._qfm = QtGui.QFontMetrics(self._qfont)
|
self._qfm = QtGui.QFontMetrics(self._qfont)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -109,11 +109,7 @@ class DpiAwareFont:
|
||||||
def px_size(self) -> int:
|
def px_size(self) -> int:
|
||||||
return self._qfont.pixelSize()
|
return self._qfont.pixelSize()
|
||||||
|
|
||||||
def configure_to_dpi(
|
def configure_to_dpi(self, screen: QtGui.QScreen | None = None):
|
||||||
self,
|
|
||||||
screen: QtGui.QScreen | None = None,
|
|
||||||
zoom_level: float = 1.0,
|
|
||||||
):
|
|
||||||
'''
|
'''
|
||||||
Set an appropriately sized font size depending on the screen DPI.
|
Set an appropriately sized font size depending on the screen DPI.
|
||||||
|
|
||||||
|
|
@ -122,7 +118,7 @@ class DpiAwareFont:
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if self._font_size is not None:
|
if self._font_size is not None:
|
||||||
self._set_qfont_px_size(self._font_size * zoom_level)
|
self._set_qfont_px_size(self._font_size)
|
||||||
return
|
return
|
||||||
|
|
||||||
# NOTE: if no font size set either in the [ui] section of the
|
# NOTE: if no font size set either in the [ui] section of the
|
||||||
|
|
@ -188,13 +184,9 @@ class DpiAwareFont:
|
||||||
self._font_inches = inches
|
self._font_inches = inches
|
||||||
font_size = math.floor(inches * dpi)
|
font_size = math.floor(inches * dpi)
|
||||||
|
|
||||||
# apply zoom level multiplier
|
|
||||||
font_size = int(font_size * zoom_level)
|
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
f"screen:{screen.name()}\n"
|
f"screen:{screen.name()}\n"
|
||||||
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
|
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
|
||||||
f"zoom_level: {zoom_level}\n"
|
|
||||||
f"\nOur best guess font size is {font_size}\n"
|
f"\nOur best guess font size is {font_size}\n"
|
||||||
)
|
)
|
||||||
# apply the size
|
# apply the size
|
||||||
|
|
@ -221,12 +213,12 @@ _font = DpiAwareFont()
|
||||||
_font_small = DpiAwareFont(_font_size_key='small')
|
_font_small = DpiAwareFont(_font_size_key='small')
|
||||||
|
|
||||||
|
|
||||||
def _config_fonts_to_screen(zoom_level: float = 1.0) -> None:
|
def _config_fonts_to_screen() -> None:
|
||||||
'configure global DPI aware font sizes'
|
'configure global DPI aware font sizes'
|
||||||
|
|
||||||
global _font, _font_small
|
global _font, _font_small
|
||||||
_font.configure_to_dpi(zoom_level=zoom_level)
|
_font.configure_to_dpi()
|
||||||
_font_small.configure_to_dpi(zoom_level=zoom_level)
|
_font_small.configure_to_dpi()
|
||||||
|
|
||||||
|
|
||||||
# TODO: re-compute font size when main widget switches screens?
|
# TODO: re-compute font size when main widget switches screens?
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
Qt main window singletons and stuff.
|
Qt main window singletons and stuff.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
|
|
@ -38,8 +37,6 @@ from piker.ui.qt import (
|
||||||
QStatusBar,
|
QStatusBar,
|
||||||
QScreen,
|
QScreen,
|
||||||
QCloseEvent,
|
QCloseEvent,
|
||||||
QEvent,
|
|
||||||
QObject,
|
|
||||||
)
|
)
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._style import _font_small, hcolor
|
from ._style import _font_small, hcolor
|
||||||
|
|
@ -49,68 +46,6 @@ from ._chart import GodWidget
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GlobalZoomEventFilter(QObject):
|
|
||||||
'''
|
|
||||||
Application-level event filter for global UI zoom shortcuts.
|
|
||||||
|
|
||||||
This filter intercepts keyboard events BEFORE they reach widgets,
|
|
||||||
allowing us to implement global UI zoom shortcuts that take precedence
|
|
||||||
over widget-specific shortcuts.
|
|
||||||
|
|
||||||
Shortcuts:
|
|
||||||
- Ctrl+Shift+Plus/Equal: Zoom in
|
|
||||||
- Ctrl+Shift+Minus: Zoom out
|
|
||||||
- Ctrl+Shift+0: Reset zoom
|
|
||||||
|
|
||||||
'''
|
|
||||||
def __init__(self, main_window: MainWindow):
|
|
||||||
super().__init__()
|
|
||||||
self.main_window = main_window
|
|
||||||
|
|
||||||
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
|
||||||
'''
|
|
||||||
Filter keyboard events for global zoom shortcuts.
|
|
||||||
|
|
||||||
Returns True to filter out (consume) the event, False to pass through.
|
|
||||||
'''
|
|
||||||
if event.type() == QEvent.Type.KeyPress:
|
|
||||||
key = event.key()
|
|
||||||
mods = event.modifiers()
|
|
||||||
|
|
||||||
# Mask out the KeypadModifier which Qt sometimes adds
|
|
||||||
mods = mods & ~Qt.KeyboardModifier.KeypadModifier
|
|
||||||
|
|
||||||
# Check if we have Ctrl+Shift (both required)
|
|
||||||
has_ctrl = bool(mods & Qt.KeyboardModifier.ControlModifier)
|
|
||||||
has_shift = bool(mods & Qt.KeyboardModifier.ShiftModifier)
|
|
||||||
|
|
||||||
# Only handle UI zoom if BOTH Ctrl and Shift are pressed
|
|
||||||
# For Plus key: user presses Cmd+Shift+Equal (which makes Plus)
|
|
||||||
# For Minus key: user presses Cmd+Shift+Minus
|
|
||||||
if has_ctrl and has_shift:
|
|
||||||
# Zoom in: Ctrl+Shift+Plus
|
|
||||||
# Note: Plus key usually comes as Key_Equal with Shift modifier
|
|
||||||
if key in (Qt.Key.Key_Plus, Qt.Key.Key_Equal):
|
|
||||||
self.main_window.zoom_in()
|
|
||||||
return True # consume event
|
|
||||||
|
|
||||||
# Zoom out: Ctrl+Shift+Minus
|
|
||||||
elif key == Qt.Key.Key_Minus:
|
|
||||||
self.main_window.zoom_out()
|
|
||||||
return True # consume event
|
|
||||||
|
|
||||||
# Reset zoom: Ctrl+Shift+0
|
|
||||||
elif key == Qt.Key.Key_0:
|
|
||||||
self.main_window.reset_zoom()
|
|
||||||
return True # consume event
|
|
||||||
|
|
||||||
# Pass through if only Ctrl (no Shift) - this goes to chart zoom
|
|
||||||
# Pass through all other events too
|
|
||||||
return False
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class MultiStatus:
|
class MultiStatus:
|
||||||
|
|
||||||
bar: QStatusBar
|
bar: QStatusBar
|
||||||
|
|
@ -246,24 +181,6 @@ class MainWindow(QMainWindow):
|
||||||
self._status_label: QLabel = None
|
self._status_label: QLabel = None
|
||||||
self._size: tuple[int, int] | None = None
|
self._size: tuple[int, int] | None = None
|
||||||
|
|
||||||
# zoom level for UI scaling (1.0 = 100%, 1.5 = 150%, etc)
|
|
||||||
# Change this value to set the default startup zoom level
|
|
||||||
self._zoom_level: float = 4.0 # Start at 200% zoom
|
|
||||||
self._min_zoom: float = 0.5
|
|
||||||
self._max_zoom: float = 10.0
|
|
||||||
self._zoom_step: float = 1.0
|
|
||||||
|
|
||||||
# event filter for global zoom shortcuts
|
|
||||||
self._zoom_filter: GlobalZoomEventFilter | None = None
|
|
||||||
|
|
||||||
def install_global_zoom_filter(self) -> None:
|
|
||||||
'''Install application-level event filter for global UI zoom shortcuts.'''
|
|
||||||
if self._zoom_filter is None:
|
|
||||||
self._zoom_filter = GlobalZoomEventFilter(self)
|
|
||||||
app = QApplication.instance()
|
|
||||||
app.installEventFilter(self._zoom_filter)
|
|
||||||
log.info('Installed global zoom shortcuts: Ctrl+Shift+Plus/Minus/0')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mode_label(self) -> QLabel:
|
def mode_label(self) -> QLabel:
|
||||||
|
|
||||||
|
|
@ -419,161 +336,6 @@ class MainWindow(QMainWindow):
|
||||||
self.godwidget.on_win_resize(event)
|
self.godwidget.on_win_resize(event)
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
def zoom_in(self) -> None:
|
|
||||||
'''Increase UI zoom level.'''
|
|
||||||
new_zoom = min(self._zoom_level + self._zoom_step, self._max_zoom)
|
|
||||||
if new_zoom != self._zoom_level:
|
|
||||||
self._zoom_level = new_zoom
|
|
||||||
self._apply_zoom()
|
|
||||||
log.info(f'Zoomed in to {self._zoom_level:.1%}')
|
|
||||||
|
|
||||||
def zoom_out(self) -> None:
|
|
||||||
'''Decrease UI zoom level.'''
|
|
||||||
new_zoom = max(self._zoom_level - self._zoom_step, self._min_zoom)
|
|
||||||
if new_zoom != self._zoom_level:
|
|
||||||
self._zoom_level = new_zoom
|
|
||||||
self._apply_zoom()
|
|
||||||
log.info(f'Zoomed out to {self._zoom_level:.1%}')
|
|
||||||
|
|
||||||
def reset_zoom(self) -> None:
|
|
||||||
'''Reset UI zoom to 100%.'''
|
|
||||||
if self._zoom_level != 1.0:
|
|
||||||
self._zoom_level = 1.0
|
|
||||||
self._apply_zoom()
|
|
||||||
log.info('Reset zoom to 100%')
|
|
||||||
|
|
||||||
def _apply_zoom(self) -> None:
|
|
||||||
'''Apply current zoom level to all UI elements.'''
|
|
||||||
from . import _style
|
|
||||||
|
|
||||||
# reconfigure fonts with zoom multiplier
|
|
||||||
_style._config_fonts_to_screen(zoom_level=self._zoom_level)
|
|
||||||
|
|
||||||
# update status bar styling with new font size
|
|
||||||
if self._status_bar:
|
|
||||||
sb = self.statusBar()
|
|
||||||
sb.setStyleSheet((
|
|
||||||
f"color : {hcolor('gunmetal')};"
|
|
||||||
f"background : {hcolor('default_dark')};"
|
|
||||||
f"font-size : {_style._font_small.px_size}px;"
|
|
||||||
"padding : 0px;"
|
|
||||||
))
|
|
||||||
|
|
||||||
# force update of mode label if it exists
|
|
||||||
if self._status_label:
|
|
||||||
self._status_label.setFont(_style._font_small.font)
|
|
||||||
|
|
||||||
# update godwidget and its children
|
|
||||||
if self.godwidget:
|
|
||||||
# update search widget if it exists
|
|
||||||
if hasattr(self.godwidget, 'search') and self.godwidget.search:
|
|
||||||
self.godwidget.search.update_fonts()
|
|
||||||
|
|
||||||
# update order mode panes in all chart views
|
|
||||||
self._update_chart_order_panes()
|
|
||||||
|
|
||||||
# recursively update all other widgets with stylesheets
|
|
||||||
self._refresh_widget_fonts(self.godwidget)
|
|
||||||
self.godwidget.update()
|
|
||||||
|
|
||||||
def _update_chart_order_panes(self) -> None:
|
|
||||||
'''Update order entry panels in all charts.'''
|
|
||||||
if not self.godwidget:
|
|
||||||
return
|
|
||||||
|
|
||||||
# iterate through all linked splits (hist and rt)
|
|
||||||
for splits_name in ['hist_linked', 'rt_linked']:
|
|
||||||
splits = getattr(self.godwidget, splits_name, None)
|
|
||||||
if not splits:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# get main chart
|
|
||||||
chart = getattr(splits, 'chart', None)
|
|
||||||
if chart:
|
|
||||||
# update axes
|
|
||||||
self._update_chart_axes(chart)
|
|
||||||
|
|
||||||
# update order pane
|
|
||||||
if hasattr(chart, 'view'):
|
|
||||||
view = chart.view
|
|
||||||
if hasattr(view, 'order_mode') and view.order_mode:
|
|
||||||
order_mode = view.order_mode
|
|
||||||
if hasattr(order_mode, 'pane') and order_mode.pane:
|
|
||||||
order_mode.pane.update_fonts()
|
|
||||||
|
|
||||||
# also check subplots
|
|
||||||
subplots = getattr(splits, 'subplots', {})
|
|
||||||
for name, subplot_chart in subplots.items():
|
|
||||||
# update subplot axes
|
|
||||||
self._update_chart_axes(subplot_chart)
|
|
||||||
|
|
||||||
# update subplot order pane
|
|
||||||
if hasattr(subplot_chart, 'view'):
|
|
||||||
subplot_view = subplot_chart.view
|
|
||||||
if hasattr(subplot_view, 'order_mode') and subplot_view.order_mode:
|
|
||||||
subplot_order_mode = subplot_view.order_mode
|
|
||||||
if hasattr(subplot_order_mode, 'pane') and subplot_order_mode.pane:
|
|
||||||
subplot_order_mode.pane.update_fonts()
|
|
||||||
|
|
||||||
# resize all sidepanes to match main chart's sidepane width
|
|
||||||
# this ensures volume/subplot sidepanes match the main chart
|
|
||||||
if splits and hasattr(splits, 'resize_sidepanes'):
|
|
||||||
splits.resize_sidepanes()
|
|
||||||
|
|
||||||
def _update_chart_axes(self, chart) -> None:
|
|
||||||
'''Update axis fonts and sizing for a chart.'''
|
|
||||||
from . import _style
|
|
||||||
|
|
||||||
# update price axis (right side)
|
|
||||||
if hasattr(chart, 'pi') and chart.pi:
|
|
||||||
plot_item = chart.pi
|
|
||||||
# get all axes from plot item
|
|
||||||
for axis_name in ['left', 'right', 'bottom', 'top']:
|
|
||||||
axis = plot_item.getAxis(axis_name)
|
|
||||||
if axis and hasattr(axis, 'update_fonts'):
|
|
||||||
axis.update_fonts(_style._font)
|
|
||||||
|
|
||||||
# force plot item to recalculate its entire layout
|
|
||||||
plot_item.updateGeometry()
|
|
||||||
|
|
||||||
# force chart widget to update
|
|
||||||
if hasattr(chart, 'updateGeometry'):
|
|
||||||
chart.updateGeometry()
|
|
||||||
|
|
||||||
# trigger a full scene update
|
|
||||||
if hasattr(chart, 'update'):
|
|
||||||
chart.update()
|
|
||||||
|
|
||||||
def _refresh_widget_fonts(self, widget: QWidget) -> None:
|
|
||||||
'''
|
|
||||||
Recursively update font sizes in all child widgets.
|
|
||||||
|
|
||||||
This handles widgets that have font-size hardcoded in their stylesheets.
|
|
||||||
'''
|
|
||||||
from . import _style
|
|
||||||
|
|
||||||
# recursively process all children
|
|
||||||
for child in widget.findChildren(QWidget):
|
|
||||||
# skip widgets that have their own update_fonts method (handled separately)
|
|
||||||
if hasattr(child, 'update_fonts'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# update child's stylesheet if it has font-size
|
|
||||||
child_stylesheet = child.styleSheet()
|
|
||||||
if child_stylesheet and 'font-size' in child_stylesheet:
|
|
||||||
# for labels and simple widgets, regenerate stylesheet
|
|
||||||
# this is a heuristic - may need refinement
|
|
||||||
try:
|
|
||||||
child.setFont(_style._font.font)
|
|
||||||
except (AttributeError, RuntimeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# update child's font
|
|
||||||
try:
|
|
||||||
child.setFont(_style._font.font)
|
|
||||||
except (AttributeError, RuntimeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# singleton app per actor
|
# singleton app per actor
|
||||||
_qt_win: QMainWindow = None
|
_qt_win: QMainWindow = None
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ from PyQt6.QtCore import (
|
||||||
QSize,
|
QSize,
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
QItemSelectionModel,
|
QItemSelectionModel,
|
||||||
QObject,
|
|
||||||
pyqtBoundSignal,
|
pyqtBoundSignal,
|
||||||
pyqtRemoveInputHook,
|
pyqtRemoveInputHook,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
20
pikerd.sh
20
pikerd.sh
|
|
@ -1,20 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# macOS wrapper for pikerd to handle missing XDG_RUNTIME_DIR
|
|
||||||
|
|
||||||
# Set up runtime directory for macOS if not already set
|
|
||||||
if [ -z "$XDG_RUNTIME_DIR" ]; then
|
|
||||||
# Use macOS standard temp directory with user-specific subdirectory
|
|
||||||
export XDG_RUNTIME_DIR="/tmp/piker-runtime-$(id -u)"
|
|
||||||
|
|
||||||
# Create the directory if it doesn't exist
|
|
||||||
if [ ! -d "$XDG_RUNTIME_DIR" ]; then
|
|
||||||
mkdir -p "$XDG_RUNTIME_DIR"
|
|
||||||
# Set proper permissions (only user can access)
|
|
||||||
chmod 700 "$XDG_RUNTIME_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Set XDG_RUNTIME_DIR to: $XDG_RUNTIME_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run pikerd with all passed arguments
|
|
||||||
exec uv run pikerd "$@"
|
|
||||||
Loading…
Reference in New Issue