code release

This commit is contained in:
iperov 2021-07-23 17:34:49 +04:00
commit a902f11f74
354 changed files with 826570 additions and 1 deletions

36
xlib/qt/__init__.py Normal file
View file

@ -0,0 +1,36 @@
from .core.QXTimeLine import QXTimeLine
from .core.QXTimer import QXTimer
from .core.widget import (BlockSignals, disable, enable, hide,
hide_and_disable, show, show_and_enable)
from .gui.from_file import (QIcon_from_file, QPixmap_from_file,
QXImage_from_file, QXPixmap_from_file)
from .gui.from_np import QPixmap_from_np
from .gui.QXImageSequence import QXImageSequence
from .gui.QXPixmap import QXPixmap
from .widgets.QXCheckBox import QXCheckBox
from .widgets.QXCollapsibleSection import QXCollapsibleSection
from .widgets.QXComboBox import QXComboBox
from .widgets.QXDirDialog import QXDirDialog
from .widgets.QXDoubleSpinBox import QXDoubleSpinBox
from .widgets.QXFileDialog import QXFileDialog
from .widgets.QXFixedLayeredImages import QXFixedLayeredImages
from .widgets.QXFrame import QXFrame
from .widgets.QXGridLayout import QXGridLayout
from .widgets.QXHBoxLayout import QXHBoxLayout
from .widgets.QXHorizontalLine import QXHorizontalLine
from .widgets.QXLabel import QXLabel
from .widgets.QXLineEdit import QXLineEdit
from .widgets.QXMainApplication import QXMainApplication
from .widgets.QXMenuBar import QXMenuBar
from .widgets.QXProgressBar import QXProgressBar
from .widgets.QXPushButton import QXPushButton
from .widgets.QXRadioButton import QXRadioButton
from .widgets.QXSaveableComboBox import QXSaveableComboBox
from .widgets.QXScrollArea import QXScrollArea
from .widgets.QXSlider import QXSlider
from .widgets.QXSpinBox import QXSpinBox
from .widgets.QXToolButton import QXToolButton
from .widgets.QXVBoxLayout import QXVBoxLayout
from .widgets.QXVerticalLine import QXVerticalLine
from .widgets.QXWidget import QXWidget
from .widgets.QXWindow import QXWindow

View file

@ -0,0 +1,261 @@
import multiprocessing
import sys
import time
import traceback
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from .qtex import *
class QSubprocessor(object):
"""
"""
class Cli(object):
def __init__ ( self, client_dict ):
s2c = multiprocessing.Queue()
c2s = multiprocessing.Queue()
self.p = multiprocessing.Process(target=self._subprocess_run, args=(client_dict,s2c,c2s) )
self.s2c = s2c
self.c2s = c2s
self.p.daemon = True
self.p.start()
self.state = None
self.sent_time = None
self.sent_data = None
self.name = None
self.host_dict = None
def kill(self):
self.p.terminate()
self.p.join()
#overridable optional
def on_initialize(self, client_dict):
#initialize your subprocess here using client_dict
pass
#overridable optional
def on_finalize(self):
#finalize your subprocess here
pass
#overridable
def process_data(self, data):
#process 'data' given from host and return result
raise NotImplementedError
#overridable optional
def get_data_name (self, data):
#return string identificator of your 'data'
return "undefined"
def log_info(self, msg): self.c2s.put ( {'op': 'log_info', 'msg':msg } )
def log_err(self, msg): self.c2s.put ( {'op': 'log_err' , 'msg':msg } )
def progress_bar_inc(self, c): self.c2s.put ( {'op': 'progress_bar_inc' , 'c':c } )
def _subprocess_run(self, client_dict, s2c, c2s):
self.c2s = c2s
data = None
try:
self.on_initialize(client_dict)
c2s.put ( {'op': 'init_ok'} )
while True:
msg = s2c.get()
op = msg.get('op','')
if op == 'data':
data = msg['data']
result = self.process_data (data)
c2s.put ( {'op': 'success', 'data' : data, 'result' : result} )
data = None
elif op == 'close':
break
time.sleep(0.001)
self.on_finalize()
c2s.put ( {'op': 'finalized'} )
except Exception as e:
c2s.put ( {'op': 'error', 'data' : data} )
if data is not None:
print ('Exception while process data [%s]: %s' % (self.get_data_name(data), traceback.format_exc()) )
else:
print ('Exception: %s' % (traceback.format_exc()) )
c2s.close()
s2c.close()
self.c2s = None
# disable pickling
def __getstate__(self):
return dict()
def __setstate__(self, d):
self.__dict__.update(d)
#overridable
def __init__(self, name, SubprocessorCli_class, no_response_time_sec = 0, io_loop_sleep_time=0.005):
if not issubclass(SubprocessorCli_class, QSubprocessor.Cli):
raise ValueError("SubprocessorCli_class must be subclass of QSubprocessor.Cli")
self.name = name
self.SubprocessorCli_class = SubprocessorCli_class
self.no_response_time_sec = no_response_time_sec
self.io_loop_sleep_time = io_loop_sleep_time
self.clis = []
#getting info about name of subprocesses, host and client dicts, and spawning them
for name, host_dict, client_dict in self.process_info_generator():
try:
cli = self.SubprocessorCli_class(client_dict)
cli.state = 1
cli.sent_time = 0
cli.sent_data = None
cli.name = name
cli.host_dict = host_dict
self.clis.append (cli)
except:
raise Exception (f"Unable to start subprocess {name}. Error: {traceback.format_exc()}")
if len(self.clis) == 0:
raise Exception ("Unable to start QSubprocessor '%s' " % (self.name))
#waiting subprocesses their success(or not) initialization
while True:
for cli in self.clis[:]:
while not cli.c2s.empty():
obj = cli.c2s.get()
op = obj.get('op','')
if op == 'init_ok':
cli.state = 0
elif op == 'log_info':
print(obj['msg'])
elif op == 'log_err':
print(obj['msg'])
elif op == 'error':
cli.kill()
self.clis.remove(cli)
break
if all ([cli.state == 0 for cli in self.clis]):
break
time.sleep(0.005)
if len(self.clis) == 0:
raise Exception ( "Unable to start subprocesses." )
#ok some processes survived, initialize host logic
self.on_clients_initialized()
self.q_timer = QTimer()
self.q_timer.timeout.connect(self.tick)
self.q_timer.start(5)
#overridable
def process_info_generator(self):
#yield per process (name, host_dict, client_dict)
for i in range(min(multiprocessing.cpu_count(), 8) ):
yield 'CPU%d' % (i), {}, {}
#overridable optional
def on_clients_initialized(self):
#logic when all subprocesses initialized and ready
pass
#overridable optional
def on_clients_finalized(self):
#logic when all subprocess finalized
pass
#overridable
def get_data(self, host_dict):
#return data for processing here
raise NotImplementedError
#overridable
def on_data_return (self, host_dict, data):
#you have to place returned 'data' back to your queue
raise NotImplementedError
#overridable
def on_result (self, host_dict, data, result):
#your logic what to do with 'result' of 'data'
raise NotImplementedError
def tick(self):
for cli in self.clis[:]:
while not cli.c2s.empty():
obj = cli.c2s.get()
op = obj.get('op','')
if op == 'success':
#success processed data, return data and result to on_result
self.on_result (cli.host_dict, obj['data'], obj['result'])
self.sent_data = None
cli.state = 0
elif op == 'error':
#some error occured while process data, returning chunk to on_data_return
if 'data' in obj.keys():
self.on_data_return (cli.host_dict, obj['data'] )
#and killing process
cli.kill()
self.clis.remove(cli)
elif op == 'log_info':
print(obj['msg'])
elif op == 'log_err':
print(obj['msg'])
elif op == 'progress_bar_inc':
...
#io.progress_bar_inc(obj['c'])
for cli in self.clis[:]:
if cli.state == 1:
if cli.sent_time != 0 and self.no_response_time_sec != 0 and (time.time() - cli.sent_time) > self.no_response_time_sec:
#subprocess busy too long
print ( '%s doesnt response, terminating it.' % (cli.name) )
self.on_data_return (cli.host_dict, cli.sent_data )
cli.kill()
self.clis.remove(cli)
for cli in self.clis[:]:
if cli.state == 0:
#free state of subprocess, get some data from get_data
data = self.get_data(cli.host_dict)
if data is not None:
#and send it to subprocess
cli.s2c.put ( {'op': 'data', 'data' : data} )
cli.sent_time = time.time()
cli.sent_data = data
cli.state = 1
if all ([cli.state == 0 for cli in self.clis]):
#gracefully terminating subprocesses
for cli in self.clis[:]:
cli.s2c.put ( {'op': 'close'} )
cli.sent_time = time.time()
while True:
for cli in self.clis[:]:
terminate_it = False
while not cli.c2s.empty():
obj = cli.c2s.get()
obj_op = obj['op']
if obj_op == 'finalized':
terminate_it = True
break
if (time.time() - cli.sent_time) > 30:
terminate_it = True
if terminate_it:
cli.state = 2
cli.kill()
if all ([cli.state == 2 for cli in self.clis]):
break
#finalizing host logic
self.q_timer.stop()
self.q_timer = None
self.on_clients_finalized()

235
xlib/qt/_unused/_unused.py Normal file
View file

@ -0,0 +1,235 @@
# from PyQt6.QtCore import *
# from PyQt6.QtGui import *
# from PyQt6.QtWidgets import *
# #from localization import StringsDB
# from .QXMainWindow import *
# class QXIconButton(QPushButton):
# """
# Custom Icon button that works through keyEvent system, without shortcut of QAction
# works only with QXMainWindow as global window class
# currently works only with one-key shortcut
# """
# def __init__(self, icon,
# tooltip=None,
# shortcut=None,
# click_func=None,
# first_repeat_delay=300,
# repeat_delay=20,
# ):
# super().__init__(icon, "")
# self.setIcon(icon)
# if shortcut is not None:
# tooltip = f"{tooltip} ( S_HOT_KEY: {shortcut} )"
# self.setToolTip(tooltip)
# self.seq = QKeySequence(shortcut) if shortcut is not None else None
# QXMainWindow.inst.add_keyPressEvent_listener ( self.on_keyPressEvent )
# QXMainWindow.inst.add_keyReleaseEvent_listener ( self.on_keyReleaseEvent )
# self.click_func = click_func
# self.first_repeat_delay = first_repeat_delay
# self.repeat_delay = repeat_delay
# self.repeat_timer = None
# self.op_device = None
# self.pressed.connect( lambda : self.action(is_pressed=True) )
# self.released.connect( lambda : self.action(is_pressed=False) )
# def action(self, is_pressed=None, op_device=None):
# if self.click_func is None:
# return
# if is_pressed is not None:
# if is_pressed:
# if self.repeat_timer is None:
# self.click_func()
# self.repeat_timer = QTimer()
# self.repeat_timer.timeout.connect(self.action)
# self.repeat_timer.start(self.first_repeat_delay)
# else:
# if self.repeat_timer is not None:
# self.repeat_timer.stop()
# self.repeat_timer = None
# else:
# self.click_func()
# if self.repeat_timer is not None:
# self.repeat_timer.setInterval(self.repeat_delay)
# def on_keyPressEvent(self, ev):
# key = ev.nativeVirtualKey()
# if ev.isAutoRepeat():
# return
# if self.seq is not None:
# if key == self.seq[0]:
# self.action(is_pressed=True)
# def on_keyReleaseEvent(self, ev):
# key = ev.nativeVirtualKey()
# if ev.isAutoRepeat():
# return
# if self.seq is not None:
# if key == self.seq[0]:
# self.action(is_pressed=False)
############################
############################
############################
############################
############################
# class QXTabWidget(QTabWidget):
# def __init__(self, tabs=None, tab_shape=None, size_policy=None, maximum_width=None, hided=False, enabled=True):
# super().__init__()
# if tabs is not None:
# for tab,icon,name in tabs:
# self.addTab(tab, icon, name)
# if tab_shape is not None:
# self.setTabShape(tab_shape)
# if maximum_width is not None:
# self.setMaximumWidth(maximum_width)
# if size_policy is not None:
# self.setSizePolicy(*size_policy)
# if hided:
# self.hide()
# self.setEnabled(enabled)
# class QXComboObjectBox(QXComboBox):
# """
# as QComboBox but str'able Iterable of objects
# and more functionality
# """
# def __init__(self, choices : Iterable, none_choice=None, font=None, size_policy=None, maximum_width=None, hided=False, enabled=True, on_choosed=None):
# super().__init__(font=font, size_policy=size_policy, maximum_width=maximum_width, hided=hided, enabled=enabled)
# self.choices = tuple(choices)
# if len(self.choices) == 0:
# raise ValueError('Number of choices are 0')
# self.none_choice = none_choice
# self.on_choosed = on_choosed
# if none_choice is not None:
# self.addItem( QIcon(), str(none_choice) )
# for i, choice in enumerate(choices):
# self.addItem( QIcon(), str(choice) )
# self.setCurrentIndex(0)
# self.currentIndexChanged.connect(self.on_toggled)
# def get_choices(self):
# return self.choices
# def get_selected_choice(self):
# idx = self.currentIndex()
# if self.none_choice is not None:
# idx -= 1
# if idx == -1:
# return None
# return self.choices[idx]
# def unselect(self, block_signals : bool = False):
# if self.none_choice is not None:
# with BlockSignals(self, block_signals=block_signals):
# self.setCurrentIndex(0)
# def set_selected_index(self, index, block_signals : bool = False):
# if index >= 0 and index < len(self.choices):
# if self.none_choice is not None:
# index += 1
# with BlockSignals(self, block_signals=block_signals):
# self.setCurrentIndex(index)
# def set_selected_choice(self, choice, block_signals : bool = False):
# with BlockSignals(self, block_signals=block_signals):
# if choice is None:
# if self.none_choice is not None:
# self.setCurrentIndex(0)
# else:
# raise ValueError('unable to change to None with none_choice=False')
# else:
# for i, schoice in enumerate(self.choices):
# if schoice == choice:
# self.setCurrentIndex(i+1)
# break
# def on_toggled(self, idx):
# if self.on_choosed is not None:
# self.on_choosed( self.get_selected_choice() )
# class QXCollapsibleSection(QWidget):
# def __init__(self, title, content_layout, is_opened=False, allow_open_close=True, show_content_frame=True):
# super().__init__()
# btn = self.btn = QToolButton()
# btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
# btn.setStyleSheet('border: none;')
# btn.setArrowType(Qt.ArrowType.RightArrow)
# btn.setText(title)
# #btn.setIconSize( QSize(8,8))
# btn.setCheckable(True)
# btn.setChecked(False)
# if allow_open_close:
# btn.toggled.connect(self.on_btn_toggled)
# line = QXFrame( size_policy=(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum) )
# line.setFrameShape(QFrame.Shape.HLine)
# frame = self.frame = QXFrame( size_policy=(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed), layout=content_layout, hided=True)
# if show_content_frame:
# frame.setFrameShape(QFrame.Shape.StyledPanel)
# main_l = QXGridLayout( contents_margins=0 )
# main_l.addWidget(btn, 0, 0, alignment=Qt.AlignmentFlag.AlignLeft)
# main_l.addWidget(QXHorizontalLine() , 0, 1)
# main_l.addWidget(frame, 1, 0, 1, 2)
# self.setLayout(main_l)
# if is_opened:
# self.open()
# else:
# self.close()
# def is_opened(self):
# return self.frame.isVisible()
# def open(self):
# self.btn.setArrowType(Qt.ArrowType.DownArrow)
# self.frame.show()
# def close(self):
# self.btn.setArrowType(Qt.ArrowType.RightArrow)
# self.frame.hide()
# def on_btn_toggled(self):
# if self.btn.isChecked():
# self.open()
# else:
# self.close()

View file

@ -0,0 +1,62 @@
import numpy as np
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
# class QActionEx(QAction):
# def __init__(self, icon, text, shortcut=None, trigger_func=None, shortcut_in_tooltip=False, is_checkable=False, is_auto_repeat=False ):
# super().__init__(icon, text)
# if shortcut is not None:
# self.setShortcut(shortcut)
# if shortcut_in_tooltip:
# self.setToolTip( f"{text} ( S_HOT_KEY : {shortcut} )")
# if trigger_func is not None:
# self.triggered.connect(trigger_func)
# if is_checkable:
# self.setCheckable(True)
# self.setAutoRepeat(is_auto_repeat)
# def QImage_from_np(img):
# if img.dtype != np.uint8:
# raise ValueError("img should be in np.uint8 format")
# h,w,c = img.shape
# if c == 1:
# fmt = QImage.Format_Grayscale8
# elif c == 3:
# fmt = QImage.Format_BGR888
# elif c == 4:
# fmt = QImage.Format_ARGB32
# else:
# raise ValueError("unsupported channel count")
# return QImage(img.data, w, h, c*w, fmt )
# def QImage_to_np(q_img, fmt=QImage.Format_BGR888):
# q_img = q_img.convertToFormat(fmt)
# width = q_img.width()
# height = q_img.height()
# b = q_img.constBits()
# b.setsize(height * width * 3)
# arr = np.frombuffer(b, np.uint8).reshape((height, width, 3))
# return arr#[::-1]
# def QPixmap_from_np(img):
# return QPixmap.fromImage(QImage_from_np(img))
# def QPoint_from_np(n):
# return QPoint(*n.astype(np.int))
# def QPoint_to_np(q):
# return np.int32( [q.x(), q.y()] )
# def QSize_to_np(q):
# return np.int32( [q.width(), q.height()] )

View file

@ -0,0 +1,43 @@
from typing import Tuple
from PyQt6.QtCore import *
_linear_easing_curve = QEasingCurve(QEasingCurve.Type.Linear)
class QXTimeLine(QTimeLine):
"""
QXTimeLine with default linear curve
frame_range(None) (int,int) start,end
"""
def __init__(self, duration,
frame_range : Tuple[int,int] = None,
loop_count=1,
update_interval : int = None,
easing_curve=None,
frameChanged=None,
stateChanged=None,
start=False):
super().__init__(duration)
if frame_range is not None:
self.setFrameRange(*frame_range)
self.setLoopCount(loop_count)
if update_interval is not None:
self.setUpdateInterval(update_interval)
if easing_curve is None:
easing_curve = _linear_easing_curve
self.setEasingCurve(easing_curve)
if frameChanged is not None:
self.frameChanged.connect(frameChanged)
if stateChanged is not None:
self.stateChanged.connect(stateChanged)
if start:
self.start()

19
xlib/qt/core/QXTimer.py Normal file
View file

@ -0,0 +1,19 @@
from PyQt6.QtCore import *
class QXTimer(QTimer):
def __init__(self, interval=None, timeout=None, single_shot=False, start=False):
super().__init__()
if interval is not None:
self.setInterval(interval)
if timeout is not None:
self.timeout.connect(timeout)
if single_shot:
self.setSingleShot(True)
if start:
self.start()

88
xlib/qt/core/widget.py Normal file
View file

@ -0,0 +1,88 @@
from collections import Iterable
from PyQt6.QtCore import *
class BlockSignals:
def __init__(self, qt_widget_or_list, block_signals=True):
if not isinstance(qt_widget_or_list, (tuple,list)):
qt_widget_or_list = [qt_widget_or_list]
self.qt_widget_or_list = qt_widget_or_list
self.block_signals = block_signals
def __enter__(self):
if self.block_signals:
for qt_widget in self.qt_widget_or_list:
qt_widget.blockSignals(True)
return self
def __exit__(self, *_):
if self.block_signals:
for qt_widget in self.qt_widget_or_list:
qt_widget.blockSignals(False)
def enable(widget_or_list):
if not isinstance(widget_or_list, (tuple,list)):
widget_or_list = [widget_or_list]
for widget in widget_or_list:
if isinstance(widget, (tuple,list)):
enable(widget)
else:
widget.setEnabled(True)
def disable(widget_or_list):
if not isinstance(widget_or_list, (tuple,list)):
widget_or_list = [widget_or_list]
for widget in widget_or_list:
if isinstance(widget, (tuple,list)):
disable(widget)
else:
widget.setEnabled(False)
def hide(widget_or_list):
if not isinstance(widget_or_list, (tuple,list)):
widget_or_list = [widget_or_list]
for widget in widget_or_list:
if isinstance(widget, (tuple,list)):
hide(widget)
else:
widget.hide()
def show(widget_or_list):
if not isinstance(widget_or_list, (tuple,list)):
widget_or_list = [widget_or_list]
for widget in widget_or_list:
if isinstance(widget, (tuple,list)):
show(widget)
else:
widget.show()
def show_and_enable(widget_or_list):
if not isinstance(widget_or_list, (tuple,list)):
widget_or_list = [widget_or_list]
for widget in widget_or_list:
if isinstance(widget, (tuple,list)):
show_and_enable(widget)
else:
widget.show()
widget.setEnabled(True)
def hide_and_disable(widget_or_list):
if not isinstance(widget_or_list, (tuple,list)):
widget_or_list = [widget_or_list]
for widget in widget_or_list:
if isinstance(widget, (tuple,list)):
hide_and_disable(widget)
else:
widget.hide()
widget.setEnabled(False)
def set_contents_margins(obj, contents_margins):
if contents_margins is not None:
if isinstance(contents_margins, int):
contents_margins = (contents_margins,)*4
if isinstance(contents_margins, Iterable):
obj.setContentsMargins(*contents_margins)

43
xlib/qt/gui/QXImage.py Normal file
View file

@ -0,0 +1,43 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from .QXPixmap import QXPixmap
class QXImage(QImage):
"""
extension of QImage
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cache = {}
def as_QXPixmap(self) -> QXPixmap:
pixmap = self._cache.get(QXPixmap, None )
if pixmap is None:
pixmap = self._cache[QXPixmap] = QXPixmap(QPixmap.fromImage(self))
return pixmap
def as_QIcon(self) -> QIcon:
icon = self._cache.get(QIcon, None )
if icon is None:
icon = self._cache[QIcon] = QIcon(self.as_QXPixmap())
return icon
def colored(self, color) -> 'QXImage':
"""
get colored version from cache or create.
"""
image = self._cache.get(color, None)
if image is None:
pixmap = self.as_QXPixmap()
qp = QPainter(pixmap)
qp.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
qp.fillRect( pixmap.rect(), QColor(color) )
qp.end()
image = self._cache[color] = QXImage( pixmap.toImage() )
return image

View file

@ -0,0 +1,24 @@
from typing import List
from .QXImage import QXImage
class QXImageSequence:
"""
contains a list of QXImage with defined FPS
"""
def __init__(self, frames : List[QXImage], fps : float):
super().__init__()
self._frames = frames
self._fps = fps
self._frame_count = len(frames)
def get_fps(self) -> float: return self._fps
def get_frame_count(self) -> int: return self._frame_count
def get_frame(self, i) -> QXImage: return self._frames[i]
def get_duration(self) -> int:
"""
return duration in ms
"""
return int( (self._frame_count / self._fps) * 1000 )

51
xlib/qt/gui/QXPixmap.py Normal file
View file

@ -0,0 +1,51 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
class QXPixmap(QPixmap):
"""
extension of QPixmap
contains cached scaled versions
cached grayscaled
cached QIcon
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cache = {}
def scaled_cached(self, width: int, height: int, aspectRatioMode: Qt.AspectRatioMode = Qt.AspectRatioMode.KeepAspectRatio) -> 'QPixmap':
"""
get scaled version from cache or create.
"""
key = (width, height)
pixmap = self._cache.get(key, None)
if pixmap is None:
pixmap = self._cache[key] = QXPixmap( self.scaled(width, height, aspectRatioMode=aspectRatioMode, transformMode=Qt.TransformationMode.SmoothTransformation) )
return pixmap
def as_QIcon(self) -> QIcon:
icon = self._cache.get( QIcon, None )
if icon is None:
icon = self._cache[QIcon] = QIcon(self)
return icon
def grayscaled_cached(self) -> 'QXPixmap':
"""
get grayscaled version from cache or create.
"""
key = 'grayscaled'
pixmap = self._cache.get(key, None)
if pixmap is None:
pixmap = QXPixmap(self)
qp = QPainter(pixmap)
qp.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
qp.fillRect( pixmap.rect(), QColor(127,127,127,255) )
qp.end()
pixmap = self._cache[key] = pixmap
return pixmap

32
xlib/qt/gui/from_file.py Normal file
View file

@ -0,0 +1,32 @@
from PyQt6.QtGui import *
from .QXImage import QXImage
from .QXPixmap import QXPixmap
def QPixmap_from_file(filepath, color=None):
img = QPixmap(str(filepath))
if color is not None:
qp = QPainter(img)
qp.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
qp.fillRect( img.rect(), QColor(color) )
qp.end()
return img
def QXPixmap_from_file(filepath, color=None):
img = QXPixmap(str(filepath))
if color is not None:
qp = QPainter(img)
qp.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
qp.fillRect( img.rect(), QColor(color) )
qp.end()
return img
def QXImage_from_file(filepath, color=None):
return QXImage(QPixmap_from_file(filepath, color).toImage())
def QIcon_from_file(filepath, color='black'):
return QIcon(QPixmap_from_file(filepath,color=color))

25
xlib/qt/gui/from_np.py Normal file
View file

@ -0,0 +1,25 @@
import numpy as np
from PyQt6.QtGui import *
from xlib.image import ImageProcessor
def QPixmap_from_np(image : np.ndarray):
ip = ImageProcessor(image).to_uint8()
N,H,W,C = ip.get_dims()
if N > 1:
raise ValueError(f'N dim must be == 1')
if C == 1:
format = QImage.Format.Format_Grayscale8
elif C == 3:
format = QImage.Format.Format_BGR888
elif C == 4:
format = QImage.Format.Format_ARGB32
else:
raise ValueError(f'Unsupported channels {C}')
image = ip.get_image('HWC')
q_image = QImage(image.data, W, H, W*C, format)
q_pixmap = QPixmap.fromImage(q_image)
return q_pixmap

View file

@ -0,0 +1,27 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXCheckBox(QCheckBox, _part_QXWidget):
def __init__(self, text=None, color=None, clicked=None, toggled=None, font=None, size_policy=None, hided=False, enabled=True):
super().__init__()
if text is not None:
self.setText(text)
if color is not None:
self.setStyleSheet(f'QCheckBox {{ color: {color};}}')
_part_QXWidget.connect_signal(clicked, self.clicked)
_part_QXWidget.connect_signal(toggled, self.toggled)
_part_QXWidget.__init__(self, font=font, size_policy=size_policy, hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,89 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from .QXFrame import QXFrame
from .QXHBoxLayout import QXHBoxLayout
from .QXLabel import QXLabel
from .QXToolButton import QXToolButton
from .QXVBoxLayout import QXVBoxLayout
class QXCollapsibleSection(QXFrame):
"""
Collapsible section.
Open/close state is saved to app db.
"""
def __init__(self, title, content_layout, vertical=False, is_opened=True, allow_open_close=True):
super().__init__()
self._is_opened = is_opened
self._vertical = vertical
if vertical:
title = '\n'.join(title)
label_title = self.label_title = QXLabel(text=title)
btn = self.btn = QXToolButton(checkable=True)
btn.setStyleSheet('border: none;')
btn.setArrowType(Qt.ArrowType.RightArrow)
btn.setChecked(False)
if allow_open_close:
btn.toggled.connect(self.on_btn_toggled)
frame = self.frame = QXFrame(layout=content_layout, size_policy=(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding), hided=True)
if vertical:
main_l = QXHBoxLayout([ ( QXFrame(layout=
QXVBoxLayout([ (btn, Qt.AlignmentFlag.AlignTop),
(label_title, Qt.AlignmentFlag.AlignCenter)
]),
size_policy=(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) ), Qt.AlignmentFlag.AlignTop),
frame ])
else:
main_l = QXVBoxLayout( [ ( QXFrame(layout=
QXHBoxLayout([ (btn, Qt.AlignmentFlag.AlignTop),
(label_title, Qt.AlignmentFlag.AlignCenter)
]),
size_policy=(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) ) , Qt.AlignmentFlag.AlignTop),
frame])
self.setLayout(main_l)
if self._is_opened:
self.open()
def _on_registered(self):
super()._on_registered()
self._is_opened = self.get_widget_data( (QXCollapsibleSection,'opened'), default_value=self._is_opened )
if self._is_opened:
self.open()
else:
self.close()
def is_opened(self):
return self.btn.isChecked()
def open(self):
self.set_widget_data( (QXCollapsibleSection,'opened'), True)
self.btn.setArrowType(Qt.ArrowType.DownArrow)
self.btn.setChecked(True)
self.frame.show()
def close(self):
self.set_widget_data( (QXCollapsibleSection,'opened'), False)
self.btn.setArrowType(Qt.ArrowType.RightArrow)
self.btn.setChecked(False)
self.frame.hide()
def on_btn_toggled(self):
if self.btn.isChecked():
self.open()
else:
self.close()

View file

@ -0,0 +1,33 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
from typing import List
class QXComboBox(QComboBox, _part_QXWidget):
def __init__(self, choices : List[str] = None,
on_index_changed=None,
font=None, tooltip_text=None,
minimum_width=None, maximum_width=None, fixed_width=None, minimum_height=None, maximum_height=None, fixed_height=None, size_policy=None, hided=False, enabled=True):
super().__init__()
if choices is not None:
for choice in choices:
self.addItem(choice)
_part_QXWidget.connect_signal(on_index_changed, self.currentIndexChanged)
_part_QXWidget.__init__(self, font=font, tooltip_text=tooltip_text,
size_policy=size_policy,
minimum_width=minimum_width, maximum_width=maximum_width,
minimum_height=minimum_height, maximum_height=maximum_height,
fixed_width=fixed_width, fixed_height=fixed_height,
hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,14 @@
from PyQt6.QtWidgets import *
class QXDirDialog(QFileDialog):
def __init__(self, parent=None, caption : str = None, directory : str = None, accepted=None):
super().__init__(parent=parent, directory=directory)
self.setOption(QFileDialog.Option.DontUseNativeDialog)
self.setOption(QFileDialog.Option.ShowDirsOnly, True)
self.setFileMode(QFileDialog.FileMode.Directory)
if caption is not None:
self.setWindowTitle(caption)
if accepted is not None:
self.accepted.connect(accepted)

View file

@ -0,0 +1,42 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXDoubleSpinBox(QDoubleSpinBox, _part_QXWidget):
def __init__(self, min=None, max=None, step=None, decimals=None, readonly=False, special_value_text=None, color=None, alignment=None, editingFinished=None, valueChanged=None, font=None, size_policy=None, hided=False, enabled=True):
super().__init__()
if min is not None:
self.setMinimum(min)
if max is not None:
self.setMaximum(max)
if step is not None:
self.setSingleStep(step)
if decimals is not None:
self.setDecimals(decimals)
if special_value_text is not None:
self.setSpecialValueText(special_value_text)
if alignment is not None:
self.setAlignment(alignment)
self.setReadOnly(readonly)
if color is not None:
self.setStyleSheet(f'QDoubleSpinBox {{ color: {color};}}\n QDoubleSpinBox::disabled {{ color: dark-gray;}}')
_part_QXWidget.connect_signal(editingFinished, self.editingFinished)
_part_QXWidget.connect_signal(valueChanged, self.valueChanged)
_part_QXWidget.__init__(self, font=font, size_policy=size_policy, hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,27 @@
from PyQt6.QtWidgets import *
class QXFileDialog(QFileDialog):
def __init__(self, parent=None,
multi_files=False,
existing_only=False,
is_save=False,
filter=None,
accepted=None):
super().__init__(parent=parent, filter=filter)
self.setOption(QFileDialog.Option.DontUseNativeDialog)
if is_save:
self.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
if multi_files:
self.setFileMode(QFileDialog.FileMode.ExistingFiles)
else:
if existing_only:
self.setFileMode(QFileDialog.FileMode.ExistingFile)
else:
self.setFileMode(QFileDialog.FileMode.AnyFile)
if accepted is not None:
self.accepted.connect(accepted)

View file

@ -0,0 +1,80 @@
from typing import List
import numpy as np
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from xlib.image import ImageProcessor
from ..gui.from_np import QPixmap_from_np
from .QXWidget import QXWidget
class QXFixedLayeredImages(QXWidget):
"""
A widget to show multiple stacked images in fixed area
"""
def __init__(self, fixed_width, fixed_height):
super().__init__()
self._fixed_width = fixed_width
self._fixed_height = fixed_height
self._qp = QPainter()
self._pixmaps : List[QPixmap] = []
def clear_images(self):
self._pixmaps : List[QPixmap] = []
self.update()
def add_image(self, image, name=None):
"""
image np.ndarray
QPixmap
all images must have the same aspect ratio
"""
if isinstance(image, np.ndarray):
ip = ImageProcessor(image)
ip.fit_in(self._fixed_width, self._fixed_height)
image = ip.get_image('HWC')
q_pixmap = QPixmap_from_np(image)
elif isinstance(image, QPixmap):
q_pixmap = image
else:
raise ValueError(f'Unsupported type of image {image.__class__}')
self._pixmaps.append(q_pixmap)
self.update()
def sizeHint(self):
return QSize(self._fixed_width, self._fixed_height)
def paintEvent(self, event):
super().paintEvent(event)
qp = self._qp
qp.begin(self)
#qp.setRenderHint(QPainter.RenderHint.Antialiasing)
qp.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
w = self._fixed_width
h = self._fixed_height
w_half = w /2
h_half = h /2
a = w/h
for pixmap in self._pixmaps:
size = pixmap.size()
ap = size.width() / size.height()
if ap > a:
ph_fit = h * (a / ap)
rect = QRect(0, h_half-ph_fit/2, w, ph_fit )
elif ap < a:
pw_fit = w * (ap / a)
rect = QRect(w_half-pw_fit/2, 0, pw_fit, h )
else:
rect = self.rect()
qp.drawPixmap(rect, pixmap, pixmap.rect())
qp.end()

View file

@ -0,0 +1,46 @@
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
from .QXMainApplication import QXMainApplication
class QXFrame(QFrame, _part_QXWidget):
def __init__(self, bg_color=None, layout=None, minimum_width=None, maximum_width=None, fixed_width=None, minimum_height=None, maximum_height=None, fixed_height=None, size_policy=None, hided=False, enabled=True):
QFrame.__init__(self)
_part_QXWidget.__init__(self, layout=layout,
size_policy=size_policy,
minimum_width=minimum_width, maximum_width=maximum_width,
minimum_height=minimum_height, maximum_height=maximum_height,
fixed_width=fixed_width, fixed_height=fixed_height,
hided=hided, enabled=enabled )
pal = QXMainApplication.get_singleton().palette()
if bg_color is not None:
bg_color = QColor(bg_color)
else:
bg_color = pal.color(QPalette.ColorRole.Window)
bg_color = QColor(bg_color.red()+12,bg_color.green()+12,bg_color.blue()+12,255)
self._bg_color = bg_color
self._qp = QPainter()
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)
def paintEvent(self, ev : QPaintEvent):
rect = self.rect()
qp = self._qp
qp.begin(self)
qp.fillRect(rect, self._bg_color )
qp.end()

View file

@ -0,0 +1,21 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ..core.widget import set_contents_margins
class QXGridLayout(QGridLayout):
def __init__(self, contents_margins=0, spacing=None, horizontal_spacing=None, vertical_spacing=None):
super().__init__()
set_contents_margins(self, contents_margins)
if spacing is not None:
self.setSpacing(spacing)
if horizontal_spacing is not None:
self.setHorizontalSpacing(horizontal_spacing)
if vertical_spacing is not None:
self.setVerticalSpacing(vertical_spacing)

View file

@ -0,0 +1,33 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ..core.widget import set_contents_margins
class QXHBoxLayout(QHBoxLayout):
def __init__(self, widgets=None, contents_margins=0, spacing=0):
super().__init__()
set_contents_margins(self, contents_margins)
if widgets is not None:
for widget in widgets:
alignment = None
if isinstance(widget, int):
thickness=widget
widget = QWidget()
widget.setFixedWidth(thickness)
widget.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)
if isinstance(widget, (tuple,list)):
widget, alignment = widget
if isinstance(widget, QLayout):
self.addLayout(widget)
else:
self.addWidget(widget)
if alignment is not None:
self.setAlignment(widget, alignment)
if spacing is not None:
self.setSpacing(spacing)

View file

@ -0,0 +1,13 @@
from PyQt6.QtWidgets import *
from .QXLabel import QXLabel
class QXHorizontalLine(QXLabel):
def __init__(self, thickness=1,
color=None):
super().__init__(size_policy=(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed),
fixed_height=thickness )
if color is not None:
self.setStyleSheet(f'background: {color};')

View file

@ -0,0 +1,74 @@
from typing import Union, Any
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ..gui import QXImage
from ._part_QXWidget import _part_QXWidget
class QXLabel(QLabel, _part_QXWidget):
def __init__(self, text = None,
color = None,
image : QXImage = None,
movie = None,
word_wrap = False, scaled_contents = False,
font=None, tooltip_text=None,
size_policy=None,
minimum_size=None, minimum_width=None, minimum_height=None,
maximum_size=None, maximum_width=None, maximum_height=None,
fixed_size=None, fixed_width=None, fixed_height=None,
hided=False, enabled=True
):
super().__init__()
self._default_pal = QPalette( self.palette() )
if text is not None:
self.setText(text)
if movie is not None:
self.setMovie(movie)
if image is not None:
self.setPixmap(image.as_QXPixmap())
if word_wrap:
self.setWordWrap(True)
self.setScaledContents(scaled_contents)
self.set_color(color)
_part_QXWidget.__init__(self, font=font, tooltip_text=tooltip_text,
size_policy=size_policy,
minimum_size=minimum_size, minimum_width=minimum_width, minimum_height=minimum_height,
maximum_size=maximum_size, maximum_width=maximum_width, maximum_height=maximum_height,
fixed_size=fixed_size, fixed_width=fixed_width, fixed_height=fixed_height,
hided=hided, enabled=enabled )
def _update_color(self):
if self._color is not None:
pal = QPalette(self._default_pal)
pal.setColor( QPalette.ColorRole.WindowText, self._color )
self.setPalette(pal)
else:
self.setPalette(self._default_pal)
def set_color(self, color : Union[Any,None] ):
self._color = QColor(color) if color is not None else None
self._update_color()
def changeEvent(self, ev : QEvent):
super().changeEvent(ev)
if ev.type() == QEvent.Type.EnabledChange:
self._update_color()
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,33 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXLineEdit(QLineEdit, _part_QXWidget):
def __init__(self, placeholder_text=None,
style_sheet=None,
read_only=False,
editingFinished=None,
font=None, size_policy=None, hided=False, enabled=True):
super().__init__()
if placeholder_text is not None:
self.setPlaceholderText(placeholder_text)
if style_sheet is not None:
self.setStyleSheet(style_sheet)
if read_only:
self.setReadOnly(True)
_part_QXWidget.connect_signal(editingFinished, self.editingFinished)
_part_QXWidget.__init__(self, font=font, size_policy=size_policy, hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,164 @@
from pathlib import Path
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from xlib import qt as lib_qt
from xlib.db import KeyValueDB
from .forward_declarations import forward_declarations
class QXMainApplication(QApplication):
"""
base class for MainApplication
QXMainApplication.inst - singleton instance
settings_dirpath(None) where the data will be saved
"""
inst : 'QXMainApplication' = None
@staticmethod
def get_singleton() -> 'QXMainApplication':
if QXMainApplication.inst is None:
raise Exception('QXMainApplication must be instantiated')
return QXMainApplication.inst
def __init__(self, app_name=None, settings_dirpath : Path = None):
super().__init__([])
if QXMainApplication.inst is not None:
raise Exception('Only one singleton QXMainApplication is allowed')
QXMainApplication.inst = self
self._settings_dirpath = settings_dirpath
if settings_dirpath is not None:
self._app_data_path = settings_dirpath / 'app.dat'
else:
self._app_data_path = None
self._hierarchy_name_count = {}
self._app_db = KeyValueDB(self._app_data_path)
if app_name is not None:
self.setApplicationName(app_name)
self.setStyle('Fusion')
text_color = QColor(200,200,200)
self.setStyleSheet(f"""
QRadioButton::disabled {{
color: gray;
}}
""")
pal = QPalette()
pal.setColor(QPalette.ColorRole.Window, QColor(56, 56, 56))
pal.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))
pal.setColor(QPalette.ColorRole.AlternateBase, QColor(56, 56, 56))
pal.setColor(QPalette.ColorRole.ToolTipBase, text_color )
pal.setColor(QPalette.ColorRole.ToolTipText, text_color )
pal.setColor(QPalette.ColorRole.Text, text_color )
pal.setColor(QPalette.ColorRole.Button, QColor(56, 56, 56))
pal.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
pal.setColor(QPalette.ColorRole.PlaceholderText, Qt.GlobalColor.darkGray)
pal.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.ButtonText, text_color)
pal.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.ButtonText, text_color)
pal.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, Qt.GlobalColor.gray)
pal.setColor(QPalette.ColorRole.WindowText, text_color )
pal.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.WindowText, text_color)
pal.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.WindowText, text_color)
pal.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, Qt.GlobalColor.gray)
pal.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, Qt.GlobalColor.gray)
pal.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
pal.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
pal.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
pal.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black)
self.setPalette(pal)
self._reinitialize = False
self._timer = lib_qt.QXTimer(interval=10, timeout=self._on_10ms_timer, start=True)
def _on_10ms_timer(self):
self._app_db.process_messages()
if self._reinitialize:
self._reinitialize = False
self.on_reinitialize()
def register_QXWidget(self, widget) -> str:
"""
registers QXWidget, checks validity, returns an unique name
"""
hierarchy = []
iter_widget = widget
while True:
hierarchy.insert(0, iter_widget.__class__.__name__)
iter_parent_widget = iter_widget.parentWidget()
if iter_parent_widget is None:
break
iter_widget = iter_parent_widget
if not isinstance(iter_widget, forward_declarations.QXWindow):
raise Exception('Top widget must be a class of QXWindow')
if len(hierarchy) == 1:
# top level widgets(Windows) has no numerification
return hierarchy[0]
else:
hierarchy_name = '.'.join(hierarchy)
num = self._hierarchy_name_count.get(hierarchy_name, -1)
num = self._hierarchy_name_count[hierarchy_name] = num + 1
return f'{hierarchy_name}:{num}'
def clear_app_data(self):
"""
clear app data and reinitialize()
"""
self._app_db.clear()
self.reinitialize()
def get_app_data(self, key, default_value=None):
"""
returns picklable data by picklable key stored in app db
returns default_value if no data
"""
return self._app_db.get_value(key, default_value=default_value)
def set_app_data(self, key, value):
"""
set picklable data by picklable key stored to app db
"""
self._app_db.set_value(key, value )
def run(self):
"""
run the app
"""
self.exec()
self._app_db.finish_pending_jobs()
def reinitialize(self):
self._reinitialize = True
def on_reinitialize(self):
raise NotImplementedError()
def get_language(self) -> str:
return self.get_app_data('__app_language', 'en-US')
def set_language(self, lang : str) -> str:
return self.set_app_data('__app_language', lang)

View file

@ -0,0 +1,32 @@
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXMenuBar(QMenuBar, _part_QXWidget):
def __init__(self,
font=None, size_policy=None, minimum_width=None, maximum_width=None, fixed_width=None, minimum_height=None, maximum_height=None, fixed_height=None, hided=False, enabled=True):
QMenuBar.__init__(self)
_part_QXWidget.__init__(self, font=font,
size_policy=size_policy,
minimum_width=minimum_width, maximum_width=maximum_width,
minimum_height=minimum_height, maximum_height=maximum_height,
fixed_width=fixed_width, fixed_height=fixed_height,
hided=hided, enabled=enabled )
self.setStyleSheet(f"""
QMenuBar {{
border: 0px;
background-color: #444444;
}}
""")
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,28 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXProgressBar(QProgressBar, _part_QXWidget):
def __init__(self, min=None,
max=None,
valueChanged=None,
font=None, size_policy=None, hided=False, enabled=True, ):
super().__init__()
if min is not None:
self.setMinimum(min)
if max is not None:
self.setMaximum(max)
_part_QXWidget.connect_signal(valueChanged, self.valueChanged)
_part_QXWidget.__init__(self, font=font, size_policy=size_policy, hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,132 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ..core.QXTimeLine import QXTimeLine
from ..gui import QXImage, QXImageSequence
from ._part_QXWidget import _part_QXWidget
class QXPushButton(QPushButton, _part_QXWidget):
def __init__(self, image : QXImage = None, flat=False,
text=None, padding=4, checkable=False,
toggled=None, released=None,
font=None, tooltip_text=None, size_policy=None,
minimum_size=None, minimum_width=None, minimum_height=None,
fixed_size=None, fixed_width=None, fixed_height=None, hided=False, enabled=True
):
super().__init__()
self._image = None
self._image_sequence = None
self._tl = None
if size_policy is None:
size_policy = (QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
if text is not None:
self.setText(text)
if image is not None:
self._set_image(image)
self.setCheckable(checkable)
if flat:
self.setStyleSheet(f"""
QPushButton {{
border: 0px;
background-color: #434343;
padding: {padding}px;
}}
QPushButton:hover {{
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #434343, stop: 0.3 #515151, stop: 0.6 #515151, stop: 1.0 #434343);
}}
QPushButton:pressed {{
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #434343, stop: 0.3 #353535, stop: 0.6 #353535, stop: 1.0 #434343);
}}
""")
_part_QXWidget.connect_signal(released, self.released)
_part_QXWidget.connect_signal(toggled, self.toggled)
_part_QXWidget.__init__(self, font=font, tooltip_text=tooltip_text,
size_policy=size_policy, minimum_size=minimum_size, minimum_width=minimum_width, minimum_height=minimum_height,
fixed_size=fixed_size, fixed_width=fixed_width, fixed_height=fixed_height,
hided=hided, enabled=enabled )
def sizeHint(self) -> QSize:
return QSize(0,0)
def setText(self, text):
QPushButton.setText(self, text)
self.setMinimumWidth(self.fontMetrics().horizontalAdvance(text)+8)
new_min_height = self.fontMetrics().height()+4
min_height = self.minimumHeight()
if new_min_height > min_height:
self.setMinimumHeight(new_min_height)
def _update_icon_size(self):
if self._image is not None:
rect = self.rect()
image = self._image
w, h = rect.width(), rect.height()
rect_aspect = w / h
size = image.size()
pixmap_aspect = size.width() / size.height()
if pixmap_aspect != rect_aspect:
if pixmap_aspect > rect_aspect:
pw, ph = w, int(h * (rect_aspect / pixmap_aspect))
px, py = 0, h/2-ph/2
elif pixmap_aspect < rect_aspect:
pw, ph = int( w * (pixmap_aspect / rect_aspect) ), h
px, py = w/2-pw/2, 0
else:
px, py, pw, ph = 0, 0, w, h
self.setIconSize( QSize(pw-4,ph-4) )
def _set_image(self, image : QXImage ):
self._image = image
self.setIcon( image.as_QIcon() )
def set_image(self, image : QXImage ):
self.stop_image_sequence()
self._set_image(image)
def set_image_sequence(self, image_sequence : QXImageSequence, loop_count : int = 1):
"""
set and play pixmap sequence
"""
self._image_sequence = image_sequence
self._tl = QXTimeLine( duration=image_sequence.get_duration(),
frame_range=(0, image_sequence.get_frame_count()-1),
loop_count=0,
update_interval=int( (1.0/image_sequence.get_fps()) * 1000),
frameChanged=self._tl_frameChanged,
start=True )
def stop_image_sequence(self):
if self._tl is not None:
self._tl.stop()
self._tl = None
self._image_sequence = None
def _tl_frameChanged(self, frame_id):
self._set_image(self._image_sequence.get_frame(frame_id))
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)
self._update_icon_size()

View file

@ -0,0 +1,36 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXRadioButton(QRadioButton, _part_QXWidget):
def __init__(self, text=None,
disabled_color=None,
auto_exclusive=False,
clicked=None, toggled=None,
font=None, size_policy=None, hided=False, enabled=True):
super().__init__()
if text is not None:
self.setText(text)
self.setAutoExclusive(auto_exclusive)
style_sheet = ''
if disabled_color is not None:
style_sheet += f'QRadioButton::disabled {{color: {disabled_color};}}'
if len(style_sheet) != 0:
self.setStyleSheet(style_sheet)
_part_QXWidget.connect_signal(clicked, self.clicked)
_part_QXWidget.connect_signal(toggled, self.toggled)
_part_QXWidget.__init__(self, font=font, size_policy=size_policy, hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,47 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from typing import Iterable
from .QXComboBox import QXComboBox
from .QXMainApplication import QXMainApplication
from ..core.widget import BlockSignals
class QXSaveableComboBox(QXComboBox):
"""
a saveable QXComboBox
"""
def __init__(self, db_key, choices : Iterable, default_choice, choices_names=None, on_choice_selected = None):
self._choices = [x for x in choices]
self._default_choice = default_choice
if choices_names is None:
choices_names = [str(x) for x in choices]
self._choices_names = choices_names
if len(self._choices) != len(self._choices_names):
raise ValueError('mismatch len of choices and choices_names')
self._db_key = db_key
self._on_choice_selected = on_choice_selected
super().__init__(choices=choices_names, on_index_changed=self._index_changed)
self.set_choice( QXMainApplication.get_singleton().get_app_data (db_key) )
def set_choice(self, choice):
if choice not in self._choices:
choice = self._default_choice
QXMainApplication.get_singleton().set_app_data(self._db_key, choice)
idx = self._choices.index(choice)
if self._on_choice_selected is not None:
self._on_choice_selected(self._choices[idx], self._choices_names[idx])
with BlockSignals(self):
self.setCurrentIndex(idx)
def _index_changed(self, idx):
self.set_choice( self._choices[idx] )

View file

@ -0,0 +1,24 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXScrollArea(QScrollArea, _part_QXWidget):
def __init__(self,
size_policy=None, hided=False, enabled=True):
super().__init__()
_part_QXWidget.__init__(self, size_policy=size_policy, hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,46 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXSlider(QSlider, _part_QXWidget):
def __init__(self, orientation=None,
min=None, max=None,
tick_position=None,
tick_interval=None,
valueChanged=None,
sliderMoved=None,
sliderPressed=None,
sliderReleased=None,
size_policy=None, hided=False, enabled=True):
if orientation is not None:
super().__init__(orientation)
else:
super().__init__()
if min is not None:
self.setMinimum(min)
if max is not None:
self.setMaximum(max)
if tick_position is not None:
self.setTickPosition(tick_position)
if tick_interval is not None:
self.setTickInterval(tick_interval)
_part_QXWidget.connect_signal(valueChanged, self.valueChanged)
_part_QXWidget.connect_signal(sliderMoved, self.sliderMoved)
_part_QXWidget.connect_signal(sliderPressed, self.sliderPressed)
_part_QXWidget.connect_signal(sliderReleased, self.sliderReleased)
_part_QXWidget.__init__(self, size_policy=size_policy, hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,48 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXSpinBox(QSpinBox, _part_QXWidget):
def __init__(self, min=None,
max=None,
step=None,
special_value_text=None,
color=None,
alignment=None,
button_symbols=None, readonly=False,
editingFinished=None, textChanged=None, valueChanged=None,
font=None, size_policy=None, hided=False, enabled=True):
super().__init__()
if min is not None:
self.setMinimum(min)
if max is not None:
self.setMaximum(max)
if step is not None:
self.setSingleStep(step)
if special_value_text is not None:
self.setSpecialValueText(special_value_text)
if alignment is not None:
self.setAlignment(alignment)
if button_symbols is not None:
self.setButtonSymbols(button_symbols)
self.setReadOnly(readonly)
if color is not None:
self.setStyleSheet(f'QSpinBox {{ color: {color};}}')
_part_QXWidget.connect_signal(editingFinished, self.editingFinished)
_part_QXWidget.connect_signal(textChanged, self.textChanged)
_part_QXWidget.connect_signal(valueChanged, self.valueChanged)
_part_QXWidget.__init__(self, font=font, size_policy=size_policy, hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,32 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXToolButton(QToolButton, _part_QXWidget):
def __init__(self, text=None,
checkable=False,
toggled=None, released=None,
font=None, size_policy=None, hided=False, enabled=True):
super().__init__()
if text is not None:
self.setText(text)
self.setCheckable(checkable)
_part_QXWidget.connect_signal(released, self.released)
_part_QXWidget.connect_signal(toggled, self.toggled)
_part_QXWidget.__init__(self, font=font, size_policy=size_policy, hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,31 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ..core.widget import set_contents_margins
class QXVBoxLayout(QVBoxLayout):
def __init__(self, widgets=None, contents_margins=0, spacing=0):
super().__init__()
set_contents_margins(self, contents_margins)
if widgets is not None:
for widget in widgets:
alignment = None
if isinstance(widget, int):
thickness=widget
widget = QWidget()
widget.setFixedHeight(thickness)
widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
if isinstance(widget, (tuple,list)):
widget, alignment = widget
if isinstance(widget, QLayout):
self.addLayout(widget)
else:
self.addWidget(widget)
if alignment is not None:
self.setAlignment(widget, alignment)
if spacing is not None:
self.setSpacing(spacing)

View file

@ -0,0 +1,11 @@
from PyQt6.QtWidgets import *
from .QXLabel import QXLabel
class QXVerticalLine(QXLabel):
def __init__(self, thickness=1, color=None):
super().__init__(size_policy=(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding),
fixed_width=thickness)
if color is not None:
self.setStyleSheet(f'background: {color.name()};')

View file

@ -0,0 +1,30 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from ._part_QXWidget import _part_QXWidget
class QXWidget(QWidget, _part_QXWidget):
"""
"""
def __init__(self, layout=None, font=None, tooltip_text=None,
minimum_width=None, maximum_width=None, fixed_width=None, minimum_height=None, maximum_height=None, fixed_height=None, size_policy=None, hided=False, enabled=True):
super().__init__()
_part_QXWidget.__init__(self, layout=layout, font=font, tooltip_text=tooltip_text,
size_policy=size_policy,
minimum_width=minimum_width, maximum_width=maximum_width,
minimum_height=minimum_height, maximum_height=maximum_height,
fixed_width=fixed_width, fixed_height=fixed_height,
hided=hided, enabled=enabled )
def focusInEvent(self, ev : QFocusEvent):
super().focusInEvent(ev)
_part_QXWidget.focusInEvent(self, ev)
def resizeEvent(self, ev : QResizeEvent):
super().resizeEvent(ev)
_part_QXWidget.resizeEvent(self, ev)

View file

@ -0,0 +1,99 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from xlib.python import EventListener
from .forward_declarations import forward_declarations
from .QXMainApplication import QXMainApplication
from .QXWidget import QXWidget
class QXWindow(QXWidget):
"""
represents top widget which has no parent
"""
def __init__(self, save_load_state=False,
size_policy=None):
super().__init__(size_policy=size_policy)
self._save_load_state = save_load_state
#QXMainApplication.get_singleton().register_QXWindow(self)
#self.keyPressEvent_listeners = []
#self.keyReleaseEvent_listeners = []
self._QXW = True
self._closeEvent_ev = EventListener()
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
self._qp = QPainter()
pal = QXMainApplication.get_singleton().palette()
self._bg_color = pal.color(QPalette.ColorRole.Window)
def call_on_closeEvent(self, func_or_list):
self._closeEvent_ev.add(func_or_list)
# def add_closeEvent_func(self, func):
# self.closeEvent_funcs.append (func)
# def add_keyPressEvent_listener(self, func):
# self.keyPressEvent_listeners.append (func)
# def add_keyReleaseEvent_listener(self, func):
# self.keyReleaseEvent_listeners.append (func)
def center_on_screen(self):
widget_width, widget_height = self.size().width(), self.size().height()
screen_size = QXMainApplication.get_singleton().primaryScreen().size()
self.move( (screen_size.width() - widget_width) // 2, (screen_size.height() - widget_height) // 2 )
#def resizeEvent(self, ev : QResizeEvent):
# super().resizeEvent(ev)
def showEvent(self, ev: QShowEvent):
super().showEvent(ev)
if self._save_load_state:
geo = self.get_widget_data('geometry')
if geo is not None:
pos, size = geo
self.move(pos)
self.resize(size)
else:
self.center_on_screen()
def hideEvent(self, ev: QHideEvent):
super().hideEvent(ev)
if self._save_load_state:
self.set_widget_data('geometry', ( self.pos(), self.size() ) )
def closeEvent(self, ev : QCloseEvent):
super().closeEvent(ev)
if ev.isAccepted():
self._closeEvent_ev.call()
def is_minimized(self) -> bool:
state = self.windowState()
return (state & Qt.WindowState.WindowMinimized) == Qt.WindowState.WindowMinimized
def paintEvent(self, ev : QPaintEvent):
qp = self._qp
qp.begin(self)
qp.fillRect(self.rect(), self._bg_color )
qp.end()
# def keyPressEvent(self, ev):
# super().keyPressEvent(ev)
# for func in self.keyPressEvent_listeners:
# func(ev)
# def keyReleaseEvent(self, ev):
# super().keyReleaseEvent(ev)
# for func in self.keyReleaseEvent_listeners:
# func(ev)
forward_declarations.QXWindow = QXWindow

View file

@ -0,0 +1,119 @@
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from ..core.widget import BlockSignals
from .QXMainApplication import QXMainApplication
class _part_QXWidget:
def __init__(self, layout=None,
font=None,
tooltip_text=None,
size_policy=None,
minimum_size=None, minimum_width=None, minimum_height=None,
maximum_size=None, maximum_width=None, maximum_height=None,
fixed_size=None, fixed_width=None, fixed_height=None,
hided=False, enabled=True):
self._registered = False
self._name_id = None
self._top_QXWindow = None
if font is not None:
self.setFont(font)
if tooltip_text is not None:
self.setToolTip(tooltip_text)
if size_policy is not None:
self.setSizePolicy(*size_policy)
if layout is not None:
self.setLayout(layout)
if minimum_size is not None:
minimum_width, minimum_height = minimum_size
if minimum_width is not None:
self.setMinimumWidth(minimum_width)
if minimum_height is not None:
self.setMinimumHeight(minimum_height)
if maximum_size is not None:
maximum_width, maximum_height = maximum_size
if maximum_width is not None:
self.setMaximumWidth(maximum_width)
if maximum_height is not None:
self.setMaximumHeight(maximum_height)
if fixed_size is not None:
fixed_width, fixed_height = fixed_size
if fixed_width is not None:
self.setFixedWidth(fixed_width)
if fixed_height is not None:
self.setFixedHeight(fixed_height)
if hided:
self.hide()
self.setEnabled(enabled)
def get_top_QXWindow(self) -> 'QXWindow':
if self._top_QXWindow is not None:
return self._top_QXWindow
obj = self
while True:
obj = obj.parentWidget()
if getattr(obj, '_QXW', False):
self._top_QXWindow = obj
return obj
if obj is None:
raise Exception('top_QXWindow is not found.')
def get_name_id(self) -> str:
"""
returns name_id of widget
"""
return self._name
def get_widget_data(self, key, default_value=None):
"""
Get picklable data by picklable key from widget's storage
if widget is not registered, default_value will be returned
"""
if not self._registered:
return default_value
return QXMainApplication.get_singleton().get_app_data ( (self._name_id, key), default_value=default_value )
def set_widget_data(self, key, data):
"""
Set picklable data by picklable key to widget's storage
if widget is not registered, nothing will be happened
"""
QXMainApplication.get_singleton().set_app_data ( (self._name_id, key), data )
def focusInEvent(self, ev : QFocusEvent):
if ev.reason() == Qt.FocusReason.TabFocusReason:
with BlockSignals(self):
self.clearFocus()
def resizeEvent(self, ev : QResizeEvent):
if not self._registered:
self._registered = True
self._name_id = QXMainApplication.get_singleton().register_QXWidget(self)
self._on_registered()
def _on_registered(self):
"""
called when widget is registered on QXMainApplication.
At this point you can use widget's storage.
"""
@staticmethod
def connect_signal(funcs, qt_signal):
if funcs is not None:
if not isinstance(funcs, (tuple,list)):
funcs = [funcs]
for func in funcs:
qt_signal.connect(func)

View file

@ -0,0 +1,2 @@
class forward_declarations:
QXWindow : 'QXWindow'= None