Compare commits

...

2 Commits

Author SHA1 Message Date
5b345e6641 feater: add camera buttons 2025-10-09 21:48:23 +02:00
cc37d7054c refactor: new mock gphoto 2025-10-09 21:35:50 +02:00
8 changed files with 688 additions and 233 deletions

View File

@@ -8,7 +8,7 @@ from .base_camera import BaseCamera
try:
import gphoto2 as gp # type: ignore
except:
import controllers.mock_gphoto as gp
import core.camera.mock_gphoto as gp
camera_widget_types = {
gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore

204
core/camera/mock_gphoto.py Normal file
View File

@@ -0,0 +1,204 @@
"""
Mock gphoto2 module for Windows testing and development.
Simulates gphoto2 API behavior to allow the app to run without a camera.
"""
import numpy as np
import cv2
from dataclasses import dataclass
# --- Constants (simulate gphoto2 enums) ---
GP_WIDGET_WINDOW = 0
GP_WIDGET_SECTION = 1
GP_WIDGET_TEXT = 2
GP_WIDGET_RANGE = 3
GP_WIDGET_TOGGLE = 4
GP_WIDGET_RADIO = 5
GP_WIDGET_MENU = 6
GP_WIDGET_BUTTON = 7
GP_WIDGET_DATE = 8
GP_OPERATION_NONE = 0x00
GP_OPERATION_CAPTURE_IMAGE = 0x01
GP_OPERATION_CAPTURE_VIDEO = 0x02
GP_OPERATION_CAPTURE_AUDIO = 0x04
GP_OPERATION_CAPTURE_PREVIEW = 0x08
GP_OPERATION_CONFIG = 0x10
GP_OPERATION_TRIGGER_CAPTURE = 0x20
# --- Error class ---
class GPhoto2Error(Exception):
pass
# --- Mock camera configuration widget ---
class MockWidget:
def __init__(self, name, label, wtype, value=None, choices=None):
self._name = name
self._label = label
self._type = wtype
self._value = value
self._choices = choices or []
self._children = []
def get_id(self):
return id(self)
def get_name(self):
return self._name
def get_label(self):
return self._label
def get_type(self):
return self._type
def get_value(self):
return self._value
def set_value(self, value):
if self._choices and value not in self._choices:
raise GPhoto2Error(f"Invalid value '{value}' for widget '{self._name}'")
self._value = value
def get_choices(self):
return self._choices
def count_children(self):
return len(self._children)
def get_child(self, i):
return self._children[i]
def add_child(self, child):
self._children.append(child)
# --- Mock classes for detection / abilities ---
@dataclass
class MockCameraInfo:
name: str
port: str
class MockCameraList:
def __init__(self):
self._cameras = [
MockCameraInfo("Mock Camera 1", "usb:001,002"),
MockCameraInfo("Mock Camera 2", "usb:001,003")
]
def count(self):
return len(self._cameras)
def get_name(self, i):
return self._cameras[i].name
def get_value(self, i):
return self._cameras[i].port
class CameraAbilities:
def __init__(self, operations):
self.operations = operations
class CameraAbilitiesList:
def load(self):
pass
def lookup_model(self, name):
return 0
def get_abilities(self, index):
return CameraAbilities(
GP_OPERATION_CAPTURE_IMAGE
| GP_OPERATION_CAPTURE_PREVIEW
| GP_OPERATION_CONFIG
)
class PortInfoList:
def load(self):
pass
def lookup_path(self, path):
return 0
def __getitem__(self, index):
return f"MockPortInfo({index})"
# --- Mock Camera class ---
class Camera:
def __init__(self):
self.initialized = False
self.port_info = None
def init(self):
self.initialized = True
def exit(self):
self.initialized = False
def set_port_info(self, info):
self.port_info = info
def get_config(self):
# Simulate config tree
root = MockWidget("root", "Root", GP_WIDGET_WINDOW)
iso = MockWidget("iso", "ISO", GP_WIDGET_MENU, "100", ["100", "200", "400", "800"])
shutter = MockWidget("shutter", "Shutter Speed", GP_WIDGET_MENU, "1/60", ["1/30", "1/60", "1/125"])
wb = MockWidget("whitebalance", "White Balance", GP_WIDGET_RADIO, "Auto", ["Auto", "Daylight", "Tungsten"])
root.add_child(iso)
root.add_child(shutter)
root.add_child(wb)
return root
def set_single_config(self, name, widget):
# Simulate saving a setting
print(f"[mock_gphoto] Setting '{name}' = '{widget.get_value()}'")
def capture_preview(self):
# Generate a fake image (OpenCV compatible)
frame = np.zeros((480, 640, 3), dtype=np.uint8)
cv2.putText(frame, "Mock Preview", (150, 240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
_, buf = cv2.imencode(".jpg", frame)
class MockFile:
def get_data_and_size(self_inner):
return buf.tobytes()
return MockFile()
# --- Mock detection functions ---
def gp_camera_autodetect():
return MockCameraList()
def check_result(value):
# gphoto2.check_result usually raises error if return < 0
return value
# --- API aliases to match gphoto2 ---
GP_ERROR = -1
gp_camera_autodetect = gp_camera_autodetect
check_result = check_result
Camera = Camera
CameraAbilitiesList = CameraAbilitiesList
PortInfoList = PortInfoList
GPhoto2Error = GPhoto2Error

64
ui/icons/camera_hdmi.svg Normal file
View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="800"
height="800"
viewBox="0 0 800 800"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="camera_hdmi.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="1.0805556"
inkscape:cx="-35.167095"
inkscape:cy="455.78406"
inkscape:window-width="3440"
inkscape:window-height="1369"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<rect
x="88.843188"
y="123.08483"
width="808.84319"
height="255.42416"
id="rect1" />
</defs>
<g
inkscape:label="Warstwa 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
id="text1"
style="text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#000000;stroke:#979797;stroke-width:12;stroke-linecap:round" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
x="-24.242525"
y="518.46466"
id="text4"><tspan
sodipodi:role="line"
id="tspan4"
x="-24.242525"
y="518.46466"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">HDMI</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

64
ui/icons/camera_usb.svg Normal file
View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="800"
height="800"
viewBox="0 0 800 800"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="camera_usb.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="1.0805556"
inkscape:cx="-35.167095"
inkscape:cy="455.78406"
inkscape:window-width="3440"
inkscape:window-height="1369"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<rect
x="88.843188"
y="123.08483"
width="808.84319"
height="255.42416"
id="rect1" />
</defs>
<g
inkscape:label="Warstwa 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
id="text1"
style="text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#000000;stroke:#979797;stroke-width:12;stroke-linecap:round" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
x="46.868595"
y="518.38385"
id="text4"><tspan
sodipodi:role="line"
id="tspan4"
x="46.868595"
y="518.38385"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">USB</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="800"
height="800"
viewBox="0 0 800 800"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="camera_usb_hdmi.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="1.0805556"
inkscape:cx="-35.167095"
inkscape:cy="455.78406"
inkscape:window-width="3440"
inkscape:window-height="1369"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<rect
x="88.843188"
y="123.08483"
width="808.84319"
height="255.42416"
id="rect1" />
</defs>
<g
inkscape:label="Warstwa 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
id="text1"
style="text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#000000;stroke:#979797;stroke-width:12;stroke-linecap:round" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
x="-24.243189"
y="721.06696"
id="text3"><tspan
sodipodi:role="line"
id="tspan3"
x="-24.243189"
y="721.06696"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">HDMI</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
x="46.868595"
y="298.13632"
id="text4"><tspan
sodipodi:role="line"
id="tspan4"
x="46.868595"
y="298.13632"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">USB</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.493 0.015 C 7.442 0.021,7.268 0.039,7.107 0.055 C 5.234 0.242,3.347 1.208,2.071 2.634 C 0.660 4.211,-0.057 6.168,0.009 8.253 C 0.124 11.854,2.599 14.903,6.110 15.771 C 8.169 16.280,10.433 15.917,12.227 14.791 C 14.017 13.666,15.270 11.933,15.771 9.887 C 15.943 9.186,15.983 8.829,15.983 8.000 C 15.983 7.171,15.943 6.814,15.771 6.113 C 14.979 2.878,12.315 0.498,9.000 0.064 C 8.716 0.027,7.683 -0.006,7.493 0.015 M8.853 1.563 C 9.967 1.707,11.010 2.136,11.944 2.834 C 12.273 3.080,12.920 3.727,13.166 4.056 C 13.727 4.807,14.142 5.690,14.330 6.535 C 14.544 7.500,14.544 8.500,14.330 9.465 C 13.916 11.326,12.605 12.978,10.867 13.828 C 10.239 14.135,9.591 14.336,8.880 14.444 C 8.456 14.509,7.544 14.509,7.120 14.444 C 5.172 14.148,3.528 13.085,2.493 11.451 C 2.279 11.114,1.999 10.526,1.859 10.119 C 1.618 9.422,1.514 8.781,1.514 8.000 C 1.514 6.961,1.715 6.075,2.160 5.160 C 2.500 4.462,2.846 3.980,3.413 3.413 C 3.980 2.846,4.462 2.500,5.160 2.160 C 6.313 1.599,7.567 1.397,8.853 1.563 M7.706 4.290 C 7.482 4.363,7.355 4.491,7.293 4.705 C 7.257 4.827,7.253 5.106,7.259 6.816 C 7.267 8.786,7.267 8.787,7.325 8.896 C 7.398 9.033,7.538 9.157,7.671 9.204 C 7.803 9.250,8.197 9.250,8.329 9.204 C 8.462 9.157,8.602 9.033,8.675 8.896 C 8.733 8.787,8.733 8.786,8.741 6.816 C 8.749 4.664,8.749 4.662,8.596 4.481 C 8.472 4.333,8.339 4.284,8.040 4.276 C 7.893 4.272,7.743 4.278,7.706 4.290 M7.786 10.530 C 7.597 10.592,7.410 10.753,7.319 10.932 C 7.249 11.072,7.237 11.325,7.294 11.495 C 7.388 11.780,7.697 12.000,8.000 12.000 C 8.303 12.000,8.612 11.780,8.706 11.495 C 8.763 11.325,8.751 11.072,8.681 10.932 C 8.616 10.804,8.460 10.646,8.333 10.580 C 8.217 10.520,7.904 10.491,7.786 10.530 " stroke="none" fill-rule="evenodd" fill="#000000"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<title>settings</title>
<desc>Created with Sketch Beta.</desc>
<defs>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="Icon-Set" sketch:type="MSLayerGroup" transform="translate(-101.000000, -360.000000)" fill="#000000">
<path d="M128.52,381.134 L127.528,382.866 C127.254,383.345 126.648,383.508 126.173,383.232 L123.418,381.628 C122.02,383.219 120.129,384.359 117.983,384.799 L117.983,387 C117.983,387.553 117.54,388 116.992,388 L115.008,388 C114.46,388 114.017,387.553 114.017,387 L114.017,384.799 C111.871,384.359 109.98,383.219 108.582,381.628 L105.827,383.232 C105.352,383.508 104.746,383.345 104.472,382.866 L103.48,381.134 C103.206,380.656 103.369,380.044 103.843,379.769 L106.609,378.157 C106.28,377.163 106.083,376.106 106.083,375 C106.083,373.894 106.28,372.838 106.609,371.843 L103.843,370.232 C103.369,369.956 103.206,369.345 103.48,368.866 L104.472,367.134 C104.746,366.656 105.352,366.492 105.827,366.768 L108.582,368.372 C109.98,366.781 111.871,365.641 114.017,365.201 L114.017,363 C114.017,362.447 114.46,362 115.008,362 L116.992,362 C117.54,362 117.983,362.447 117.983,363 L117.983,365.201 C120.129,365.641 122.02,366.781 123.418,368.372 L126.173,366.768 C126.648,366.492 127.254,366.656 127.528,367.134 L128.52,368.866 C128.794,369.345 128.631,369.956 128.157,370.232 L125.391,371.843 C125.72,372.838 125.917,373.894 125.917,375 C125.917,376.106 125.72,377.163 125.391,378.157 L128.157,379.769 C128.631,380.044 128.794,380.656 128.52,381.134 L128.52,381.134 Z M130.008,378.536 L127.685,377.184 C127.815,376.474 127.901,375.749 127.901,375 C127.901,374.252 127.815,373.526 127.685,372.816 L130.008,371.464 C130.957,370.912 131.281,369.688 130.733,368.732 L128.75,365.268 C128.203,364.312 126.989,363.983 126.041,364.536 L123.694,365.901 C122.598,364.961 121.352,364.192 119.967,363.697 L119.967,362 C119.967,360.896 119.079,360 117.983,360 L114.017,360 C112.921,360 112.033,360.896 112.033,362 L112.033,363.697 C110.648,364.192 109.402,364.961 108.306,365.901 L105.959,364.536 C105.011,363.983 103.797,364.312 103.25,365.268 L101.267,368.732 C100.719,369.688 101.044,370.912 101.992,371.464 L104.315,372.816 C104.185,373.526 104.099,374.252 104.099,375 C104.099,375.749 104.185,376.474 104.315,377.184 L101.992,378.536 C101.044,379.088 100.719,380.312 101.267,381.268 L103.25,384.732 C103.797,385.688 105.011,386.017 105.959,385.464 L108.306,384.099 C109.402,385.039 110.648,385.809 112.033,386.303 L112.033,388 C112.033,389.104 112.921,390 114.017,390 L117.983,390 C119.079,390 119.967,389.104 119.967,388 L119.967,386.303 C121.352,385.809 122.598,385.039 123.694,384.099 L126.041,385.464 C126.989,386.017 128.203,385.688 128.75,384.732 L130.733,381.268 C131.281,380.312 130.957,379.088 130.008,378.536 L130.008,378.536 Z M116,378 C114.357,378 113.025,376.657 113.025,375 C113.025,373.344 114.357,372 116,372 C117.643,372 118.975,373.344 118.975,375 C118.975,376.657 117.643,378 116,378 L116,378 Z M116,370 C113.261,370 111.042,372.238 111.042,375 C111.042,377.762 113.261,380 116,380 C118.739,380 120.959,377.762 120.959,375 C120.959,372.238 118.739,370 116,370 L116,370 Z" id="settings" sketch:type="MSShapeGroup">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -6,284 +6,313 @@ from ui.widgets.placeholder_widget import PlaceholderWidget
class ZoomableImageView(QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
def __init__(self, parent=None):
super().__init__(parent)
# Scena i element obrazu
self._scene = QGraphicsScene(self)
self.setScene(self._scene)
self._scene.setBackgroundBrush(
QBrush(QColor(20, 20, 20))) # ciemne tło
# Scena i element obrazu
self._scene = QGraphicsScene(self)
self.setScene(self._scene)
self._scene.setBackgroundBrush(
QBrush(QColor(20, 20, 20))) # ciemne tło
self._pixmap_item = QGraphicsPixmapItem()
self._scene.addItem(self._pixmap_item)
self._pixmap_item = QGraphicsPixmapItem()
self._scene.addItem(self._pixmap_item)
# Ustawienia widoku
# przesuwanie myszą
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.setRenderHint(QPainter.RenderHint.Antialiasing)
self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
# Ustawienia widoku
# przesuwanie myszą
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.setRenderHint(QPainter.RenderHint.Antialiasing)
self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
# Wyłączenie suwaków
self.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# Parametry zoomu
self._zoom_factor = 1.25
self._current_scale = 1.0
# Wyłączenie suwaków
self.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# Parametry zoomu
self._zoom_factor = 1.25
self._current_scale = 1.0
def set_image(self, pixmap: QPixmap):
# pixmap = QPixmap(image_path)
self._pixmap_item.setPixmap(pixmap)
self._scene.setSceneRect(pixmap.rect())
# self.reset_transform()
def set_image(self, pixmap: QPixmap):
# pixmap = QPixmap(image_path)
self._pixmap_item.setPixmap(pixmap)
self._scene.setSceneRect(pixmap.rect())
# self.reset_transform()
def reset_transform(self):
"""Resetuje skalowanie i ustawia 1:1"""
self._current_scale = 1.0
self.setTransform(self.transform().fromScale(1, 1))
def reset_transform(self):
"""Resetuje skalowanie i ustawia 1:1"""
self._current_scale = 1.0
self.setTransform(self.transform().fromScale(1, 1))
def wheelEvent(self, event: QWheelEvent):
"""Zoom kółkiem myszy"""
if event.modifiers() & Qt.KeyboardModifier.ControlModifier: # zoom tylko z CTRL
if event.angleDelta().y() > 0:
zoom = self._zoom_factor
else:
zoom = 1 / self._zoom_factor
def wheelEvent(self, event: QWheelEvent):
"""Zoom kółkiem myszy"""
if event.modifiers() & Qt.KeyboardModifier.ControlModifier: # zoom tylko z CTRL
if event.angleDelta().y() > 0:
zoom = self._zoom_factor
else:
zoom = 1 / self._zoom_factor
self._current_scale *= zoom
self.scale(zoom, zoom)
else:
return
super().wheelEvent(event) # normalne przewijanie
self._current_scale *= zoom
self.scale(zoom, zoom)
else:
return
super().wheelEvent(event) # normalne przewijanie
class CameraPlaceholder(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def __init__(self, parent=None):
super().__init__(parent)
self.setAutoFillBackground(True)
self.setStyleSheet("background-color: #141414;")
self.setAutoFillBackground(True)
self.setStyleSheet("background-color: #141414;")
layout = QVBoxLayout(self)
layout.setSpacing(20)
layout = QVBoxLayout(self)
layout.setSpacing(20)
self.camera_start_btn = QPushButton("Start Camera")
self.camera_start_btn.setFixedSize(200, 50)
style_sheet = """
QPushButton {
/* --- Styl podstawowy --- */
background-color: transparent;
border: 2px solid #CECECE; /* Grubość, styl i kolor obramowania */
border-radius: 25px; /* Kluczowa właściwość do zaokrąglenia rogów! */
color: #CECECE;
padding: 10px 20px; /* Wewnętrzny margines */
font-size: 16px;
}
self.camera_start_btn = QPushButton("Start Camera")
self.camera_start_btn.setFixedSize(200, 50)
style_sheet = """
QPushButton {
/* --- Styl podstawowy --- */
background-color: transparent;
border: 2px solid #CECECE; /* Grubość, styl i kolor obramowania */
border-radius: 25px; /* Kluczowa właściwość do zaokrąglenia rogów! */
color: #CECECE;
padding: 10px 20px; /* Wewnętrzny margines */
font-size: 16px;
}
QPushButton:hover {
/* --- Styl po najechaniu myszką --- */
color: #F0F0F0;
border: 2px solid #F0F0F0;
}
QPushButton:hover {
/* --- Styl po najechaniu myszką --- */
color: #F0F0F0;
border: 2px solid #F0F0F0;
}
QPushButton:pressed {
/* --- Styl po naciśnięciu --- */
background-color: #e0e0e0; /* Ciemniejsze tło w momencie kliknięcia */
border: 2px solid #e0e0e0; /* Zmiana koloru ramki dla sygnalizacji akcji */
}
"""
self.camera_start_btn.setStyleSheet(style_sheet)
QPushButton:pressed {
/* --- Styl po naciśnięciu --- */
background-color: #e0e0e0; /* Ciemniejsze tło w momencie kliknięcia */
border: 2px solid #e0e0e0; /* Zmiana koloru ramki dla sygnalizacji akcji */
}
"""
self.camera_start_btn.setStyleSheet(style_sheet)
self.info_label = QLabel("Kliknij, aby uruchomić kamerę")
self.info_label.setStyleSheet(
"background-color: transparent; color: #CECECE; font-size: 18px;")
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.info_label = QLabel("Kliknij, aby uruchomić kamerę")
self.info_label.setStyleSheet(
"background-color: transparent; color: #CECECE; font-size: 18px;")
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addStretch()
layout.addWidget(self.camera_start_btn,
alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.info_label)
layout.addStretch()
self.setLayout(layout)
layout.addStretch()
layout.addWidget(self.camera_start_btn,
alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.info_label)
layout.addStretch()
self.setLayout(layout)
def set_info_text(self, text: str):
self.info_label.setText(text)
def set_info_text(self, text: str):
self.info_label.setText(text)
class ViewWithOverlay(QWidget):
toggleOrientation = Signal()
swapViews = Signal()
rotateCW = Signal()
rotateCCW = Signal()
cameraConnection = Signal()
cameraSettings = Signal()
toggleOrientation = Signal()
swapViews = Signal()
rotateCW = Signal()
rotateCCW = Signal()
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
def __init__(self, live: bool = False):
super().__init__()
self.live = live
self.viewer = ZoomableImageView()
layout.addWidget(self.viewer)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
icon_size = QSize(32, 32)
btn_size = (48, 48)
btn_style = """
background-color: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border: 2px solid #1f1f1f;
"""
self.viewer = ZoomableImageView()
layout.addWidget(self.viewer)
self._create_top_right_buttons()
if self.live:
self._create_top_left_buttons()
self.cw_btn = QToolButton(self)
self.cw_btn.setIcon(QIcon("ui/icons/rotate-cw-svgrepo-com.svg"))
self.cw_btn.setIconSize(icon_size)
self.cw_btn.setStyleSheet(btn_style)
self.cw_btn.setFixedSize(*btn_size)
move_x = self.cw_btn.width() + 10
self.cw_btn.move(self.width() - move_x, 10)
self.cw_btn.clicked.connect(self.rotateCW)
self.resize(self.size())
self.ccw_btn = QToolButton(self)
self.ccw_btn.setIcon(QIcon("ui/icons/rotate-ccw-svgrepo-com.svg"))
self.ccw_btn.setIconSize(icon_size)
self.ccw_btn.setStyleSheet(btn_style)
self.ccw_btn.setFixedSize(*btn_size)
move_x += self.ccw_btn.width() + 10
self.ccw_btn.move(self.width() - move_x, 10)
self.ccw_btn.clicked.connect(self.rotateCCW)
# self.cw_btn.raise_()
# self.ccw_btn.raise_()
# self.flip_btn.raise_()
# self.orient_btn.raise_()
self.flip_btn = QToolButton(self)
# self.flip_btn.setIcon(QIcon("ui/icons/flip-vertical-svgrepo-com.svg"))
self.flip_btn.setIconSize(icon_size)
self.flip_btn.setStyleSheet(btn_style)
self.flip_btn.setFixedSize(*btn_size)
move_x += self.flip_btn.width() + 10
self.flip_btn.move(self.width() - move_x, 10)
self.flip_btn.clicked.connect(self.swapViews)
self.toggle_orientation(Qt.Orientation.Vertical)
self.orient_btn = QToolButton(self)
# self.orient_btn.setIcon(QIcon("ui/icons/horizontal-stacks-svgrepo-com.svg"))
self.orient_btn.setIconSize(icon_size)
self.orient_btn.setStyleSheet(btn_style)
self.orient_btn.setFixedSize(*btn_size)
move_x += self.orient_btn.width() + 10
self.orient_btn.move(self.width() - move_x, 10)
self.orient_btn.clicked.connect(self.toggleOrientation)
def _create_tool_button(self, callback, icon_path: str | None):
icon_size = QSize(32, 32)
btn_size = (48, 48)
btn_style = """
background-color: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border: 2px solid #1f1f1f;
"""
btn = QToolButton(self)
if icon_path:
btn.setIcon(QIcon(icon_path))
btn.setIconSize(icon_size)
btn.setStyleSheet(btn_style)
btn.setFixedSize(*btn_size)
btn.clicked.connect(callback)
self.cw_btn.raise_()
self.ccw_btn.raise_()
self.flip_btn.raise_()
self.orient_btn.raise_()
return btn
def _create_top_right_buttons(self):
self.cw_btn = self._create_tool_button(
icon_path="ui/icons/rotate-cw-svgrepo-com.svg",
callback=self.rotateCW,
)
self.toggle_orientation(Qt.Orientation.Vertical)
self.ccw_btn = self._create_tool_button(
icon_path="ui/icons/rotate-ccw-svgrepo-com.svg",
callback=self.rotateCCW,
)
def set_image(self, pixmap: QPixmap):
self.viewer.set_image(pixmap)
self.flip_btn = self._create_tool_button(
icon_path=None,
callback=self.swapViews,
)
def resizeEvent(self, event):
super().resizeEvent(event)
# Aktualizacja pozycji przycisku przy zmianie rozmiaru
move_x = self.cw_btn.width() + 10
self.cw_btn.move(self.width() - move_x, 10)
move_x += self.ccw_btn.width() + 10
self.ccw_btn.move(self.width() - move_x, 10)
move_x += self.flip_btn.width() + 10
self.flip_btn.move(self.width() - move_x, 10)
move_x += self.orient_btn.width() + 10
self.orient_btn.move(self.width() - move_x, 10)
self.orient_btn = self._create_tool_button(
icon_path=None,
callback=self.toggleOrientation,
)
def toggle_orientation(self, orientation):
if orientation == Qt.Orientation.Vertical:
self.flip_btn.setIcon(QIcon("ui/icons/flip-vertical-svgrepo-com.svg"))
self.orient_btn.setIcon(QIcon("ui/icons/horizontal-stacks-svgrepo-com.svg"))
else:
self.flip_btn.setIcon(QIcon("ui/icons/flip-horizontal-svgrepo-com.svg"))
self.orient_btn.setIcon(QIcon("ui/icons/vertical-stacks-svgrepo-com.svg"))
def enterEvent(self, event: QEnterEvent) -> None:
self.orient_btn.show()
self.flip_btn.show()
self.ccw_btn.show()
self.cw_btn.show()
return super().enterEvent(event)
def leaveEvent(self, event: QEvent) -> None:
self.orient_btn.hide()
self.flip_btn.hide()
self.ccw_btn.hide()
self.cw_btn.hide()
return super().leaveEvent(event)
def _create_top_left_buttons(self):
self.camera_btn = self._create_tool_button(
icon_path="ui/icons/settings-svgrepo-com.svg",
callback=self.cameraConnection
)
self.settings_btn = self._create_tool_button(
icon_path="ui/icons/error-16-svgrepo-com.svg",
callback=self.cameraSettings
)
def set_image(self, pixmap: QPixmap):
self.viewer.set_image(pixmap)
def resizeEvent(self, event):
super().resizeEvent(event)
# Aktualizacja pozycji przycisku przy zmianie rozmiaru
if self.live:
left_corner = 10
self.camera_btn.move(left_corner, 10)
left_corner += self.camera_btn.width() + 10
self.settings_btn.move(left_corner, 10)
right_corner = self.cw_btn.width() + 10
self.cw_btn.move(self.width() - right_corner, 10)
right_corner += self.ccw_btn.width() + 10
self.ccw_btn.move(self.width() - right_corner, 10)
right_corner += self.flip_btn.width() + 10
self.flip_btn.move(self.width() - right_corner, 10)
right_corner += self.orient_btn.width() + 10
self.orient_btn.move(self.width() - right_corner, 10)
def toggle_orientation(self, orientation):
if orientation == Qt.Orientation.Vertical:
self.flip_btn.setIcon(QIcon("ui/icons/flip-vertical-svgrepo-com.svg"))
self.orient_btn.setIcon(QIcon("ui/icons/horizontal-stacks-svgrepo-com.svg"))
else:
self.flip_btn.setIcon(QIcon("ui/icons/flip-horizontal-svgrepo-com.svg"))
self.orient_btn.setIcon(QIcon("ui/icons/vertical-stacks-svgrepo-com.svg"))
def enterEvent(self, event: QEnterEvent) -> None:
if self.live:
self.camera_btn.show()
self.settings_btn.show()
self.orient_btn.show()
self.flip_btn.show()
self.ccw_btn.show()
self.cw_btn.show()
return super().enterEvent(event)
def leaveEvent(self, event: QEvent) -> None:
if self.live:
self.camera_btn.hide()
self.settings_btn.hide()
self.orient_btn.hide()
self.flip_btn.hide()
self.ccw_btn.hide()
self.cw_btn.hide()
return super().leaveEvent(event)
class SplitView(QSplitter):
def __init__(self, parent=None):
super().__init__(parent)
print("Inicjalizacja SplitView2")
self.setOrientation(Qt.Orientation.Vertical)
def __init__(self, parent=None):
super().__init__(parent)
print("Inicjalizacja SplitView2")
self.setOrientation(Qt.Orientation.Vertical)
self.widget_start = CameraPlaceholder()
# self.widget_live = ZoomableImageView()
self.widget_live = ViewWithOverlay()
# self.widget_live = PlaceholderWidget("Camera View", "#750466")
# self.widget_ref = ZoomableImageView()
self.widget_ref = ViewWithOverlay()
# self.widget_ref = PlaceholderWidget("Image View", "#007981")
self.widget_start = CameraPlaceholder()
# self.widget_live = ZoomableImageView()
self.widget_live = ViewWithOverlay()
# self.widget_live = PlaceholderWidget("Camera View", "#750466")
# self.widget_ref = ZoomableImageView()
self.widget_ref = ViewWithOverlay()
# self.widget_ref = PlaceholderWidget("Image View", "#007981")
self.stack = QStackedWidget()
self.stack.addWidget(self.widget_start)
self.stack.addWidget(self.widget_live)
self.stack.setCurrentWidget(self.widget_start)
self.stack = QStackedWidget()
self.stack.addWidget(self.widget_start)
self.stack.addWidget(self.widget_live)
self.stack.setCurrentWidget(self.widget_start)
self.addWidget(self.stack)
self.addWidget(self.widget_ref)
self.addWidget(self.stack)
self.addWidget(self.widget_ref)
self.setSizes([self.height(), 0])
self.setSizes([self.height(), 0])
pixmap = QPixmap("media/empty_guitar_h.jpg")
# pixmap.fill(Qt.GlobalColor.lightGray)
self.widget_live.set_image(pixmap)
pixmap = QPixmap("media/empty_guitar_h.jpg")
# pixmap.fill(Qt.GlobalColor.lightGray)
self.widget_live.set_image(pixmap)
self.widget_live.toggleOrientation.connect(self.toggle_orientation)
self.widget_ref.toggleOrientation.connect(self.toggle_orientation)
self.widget_live.swapViews.connect(self.swap_views)
self.widget_ref.swapViews.connect(self.swap_views)
self.widget_live.toggleOrientation.connect(self.toggle_orientation)
self.widget_ref.toggleOrientation.connect(self.toggle_orientation)
self.widget_live.swapViews.connect(self.swap_views)
self.widget_ref.swapViews.connect(self.swap_views)
def toggle_orientation(self):
if self.orientation() == Qt.Orientation.Vertical:
self.setOrientation(Qt.Orientation.Horizontal)
self.setSizes([self.width()//2, self.width()//2])
self.widget_live.toggle_orientation(Qt.Orientation.Horizontal)
self.widget_ref.toggle_orientation(Qt.Orientation.Horizontal)
else:
self.setOrientation(Qt.Orientation.Vertical)
self.setSizes([self.height()//2, self.height()//2])
self.widget_live.toggle_orientation(Qt.Orientation.Vertical)
self.widget_ref.toggle_orientation(Qt.Orientation.Vertical)
def toggle_orientation(self):
if self.orientation() == Qt.Orientation.Vertical:
self.setOrientation(Qt.Orientation.Horizontal)
self.setSizes([self.width()//2, self.width()//2])
self.widget_live.toggle_orientation(Qt.Orientation.Horizontal)
self.widget_ref.toggle_orientation(Qt.Orientation.Horizontal)
else:
self.setOrientation(Qt.Orientation.Vertical)
self.setSizes([self.height()//2, self.height()//2])
self.widget_live.toggle_orientation(Qt.Orientation.Vertical)
self.widget_ref.toggle_orientation(Qt.Orientation.Vertical)
def swap_views(self):
"""Zamiana widoków miejscami"""
index_live = self.indexOf(self.stack)
index_ref = self.indexOf(self.widget_ref)
sizes = self.sizes()
self.insertWidget(index_live, self.widget_ref)
self.insertWidget(index_ref, self.stack)
self.setSizes(sizes)
def swap_views(self):
"""Zamiana widoków miejscami"""
index_live = self.indexOf(self.stack)
index_ref = self.indexOf(self.widget_ref)
sizes = self.sizes()
self.insertWidget(index_live, self.widget_ref)
self.insertWidget(index_ref, self.stack)
self.setSizes(sizes)
def set_live_image(self, pixmap: QPixmap):
"""Ustawienie obrazu na żywo"""
self.widget_live.set_image(pixmap)
if self.stack.currentWidget() != self.widget_live:
self.stack.setCurrentWidget(self.widget_live)
def set_live_image(self, pixmap: QPixmap):
"""Ustawienie obrazu na żywo"""
self.widget_live.set_image(pixmap)
if self.stack.currentWidget() != self.widget_live:
self.stack.setCurrentWidget(self.widget_live)
def set_reference_image(self, path_image: str):
"""Ustawienie obrazu referencyjnego"""
pixmap = QPixmap(path_image)
self.widget_ref.set_image(pixmap)
def set_reference_image(self, path_image: str):
"""Ustawienie obrazu referencyjnego"""
pixmap = QPixmap(path_image)
self.widget_ref.set_image(pixmap)
def toglle_live_view(self):
"""Przełączanie widoku na żywo"""
if self.stack.currentWidget() == self.widget_start:
self.stack.setCurrentWidget(self.widget_live)
else:
self.stack.setCurrentWidget(self.widget_start)
def toglle_live_view(self):
"""Przełączanie widoku na żywo"""
if self.stack.currentWidget() == self.widget_start:
self.stack.setCurrentWidget(self.widget_live)
else:
self.stack.setCurrentWidget(self.widget_start)