cfclient 2017.4__py3-none-any.whl → 2025.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cfclient/__init__.py +16 -11
- cfclient/configs/config.json +4 -3
- cfclient/configs/input/Generic_OS_X.json +1 -0
- cfclient/configs/input/Joystick.json +1 -0
- cfclient/configs/input/PS3_Mode_1.json +1 -0
- cfclient/configs/input/PS3_Mode_2.json +1 -0
- cfclient/configs/input/PS3_Mode_3.json +1 -0
- cfclient/configs/input/PS4_Mode_1.json +1 -0
- cfclient/configs/input/PS4_Mode_2.json +1 -0
- cfclient/configs/input/PS4_shoulder_btns_yaw.json +1 -0
- cfclient/configs/input/xbox360_mode1.json +1 -0
- cfclient/configs/log/PID_tuning/Attitude.json +46 -0
- cfclient/configs/log/PID_tuning/Attitude_rate.json +46 -0
- cfclient/configs/log/PID_tuning/Position.json +46 -0
- cfclient/configs/log/PID_tuning/Velocity.json +46 -0
- cfclient/configs/log/PID_tuning_components/Pitch.json +22 -0
- cfclient/configs/log/PID_tuning_components/Pitch_rate.json +22 -0
- cfclient/configs/log/PID_tuning_components/Position_x.json +22 -0
- cfclient/configs/log/PID_tuning_components/Position_y.json +22 -0
- cfclient/configs/log/PID_tuning_components/Position_z.json +22 -0
- cfclient/configs/log/PID_tuning_components/Roll.json +22 -0
- cfclient/configs/log/PID_tuning_components/Roll_rate.json +22 -0
- cfclient/configs/log/PID_tuning_components/Velocity_x.json +22 -0
- cfclient/configs/log/PID_tuning_components/Velocity_y.json +22 -0
- cfclient/configs/log/PID_tuning_components/Velocity_z.json +22 -0
- cfclient/configs/log/PID_tuning_components/Yaw.json +22 -0
- cfclient/configs/log/PID_tuning_components/Yaw_rate.json +22 -0
- cfclient/gui.py +44 -9
- cfclient/headless.py +3 -12
- cfclient/resources/log_param_doc.json +1 -0
- cfclient/ui/connectivity_manager.py +198 -0
- cfclient/ui/dialogs/about.py +53 -36
- cfclient/ui/dialogs/about.ui +23 -3
- cfclient/ui/dialogs/anchor_position_dialog.py +252 -0
- cfclient/ui/dialogs/anchor_position_dialog.ui +138 -0
- cfclient/ui/dialogs/basestation_mode_dialog.py +185 -0
- cfclient/ui/dialogs/basestation_mode_dialog.ui +186 -0
- cfclient/ui/dialogs/bootloader.py +448 -85
- cfclient/ui/dialogs/bootloader.ui +387 -134
- cfclient/ui/dialogs/cf2config.py +4 -4
- cfclient/ui/dialogs/cf2config.ui +3 -4
- cfclient/ui/dialogs/inputconfigdialogue.py +24 -19
- cfclient/ui/dialogs/inputconfigdialogue.ui +53 -30
- cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.py +220 -0
- cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.ui +110 -0
- cfclient/ui/dialogs/lighthouse_system_type_dialog.py +93 -0
- cfclient/ui/dialogs/lighthouse_system_type_dialog.ui +121 -0
- cfclient/ui/dialogs/logconfigdialogue.py +401 -101
- cfclient/ui/dialogs/logconfigdialogue.ui +117 -72
- cfclient/ui/icons/bl.webp +0 -0
- cfclient/ui/icons/bolt.webp +0 -0
- cfclient/ui/icons/cf21.webp +0 -0
- cfclient/ui/icons/checkmark_black.png +0 -0
- cfclient/ui/icons/checkmark_white.png +0 -0
- cfclient/ui/icons/create.png +0 -0
- cfclient/ui/icons/delete.png +0 -0
- cfclient/ui/icons/flapper.webp +0 -0
- cfclient/ui/icons/tag.webp +0 -0
- cfclient/ui/main.py +328 -258
- cfclient/ui/main.ui +184 -80
- cfclient/ui/pluginhelper.py +7 -1
- cfclient/ui/pose_logger.py +116 -0
- cfclient/ui/tab_toolbox.py +208 -0
- cfclient/ui/tabs/ColorLEDTab.py +752 -0
- cfclient/ui/tabs/ConsoleTab.py +48 -13
- cfclient/ui/{toolboxes → tabs}/CrtpSharkToolbox.py +19 -34
- cfclient/ui/tabs/ExampleTab.py +9 -16
- cfclient/ui/tabs/FlightTab.py +437 -325
- cfclient/ui/tabs/GpsTab.py +14 -20
- cfclient/ui/tabs/LEDRingTab.py +277 -0
- cfclient/ui/tabs/LogBlockDebugTab.py +20 -27
- cfclient/ui/tabs/LogBlockTab.py +35 -35
- cfclient/ui/tabs/LogClientTab.py +85 -0
- cfclient/ui/tabs/LogTab.py +50 -27
- cfclient/ui/tabs/ParamTab.py +443 -57
- cfclient/ui/tabs/PlotTab.py +23 -25
- cfclient/ui/tabs/TuningTab.py +292 -0
- cfclient/ui/tabs/__init__.py +12 -2
- cfclient/ui/tabs/colorLEDTab.ui +624 -0
- cfclient/ui/tabs/consoleTab.ui +46 -0
- cfclient/ui/tabs/flightActionContainer.ui +103 -0
- cfclient/ui/tabs/flightTab.ui +724 -237
- cfclient/ui/tabs/{ledTab.ui → ledRingTab.ui} +63 -46
- cfclient/ui/tabs/lighthouse_tab.py +714 -0
- cfclient/ui/tabs/lighthouse_tab.ui +430 -0
- cfclient/ui/tabs/locopositioning_tab.py +606 -389
- cfclient/ui/tabs/locopositioning_tab.ui +370 -253
- cfclient/ui/tabs/logClientTab.ui +52 -0
- cfclient/ui/tabs/logTab.ui +1 -1
- cfclient/ui/tabs/paramTab.ui +204 -3
- cfclient/ui/tabs/tuningTab.ui +773 -0
- cfclient/ui/widgets/ai.py +37 -39
- cfclient/ui/widgets/hexspinbox.py +16 -10
- cfclient/ui/widgets/plotter.ui +39 -47
- cfclient/ui/widgets/plotwidget.py +57 -22
- cfclient/ui/widgets/super_slider.py +112 -0
- cfclient/ui/wizards/__init__.py +0 -0
- cfclient/ui/wizards/bslh_1.png +0 -0
- cfclient/ui/wizards/bslh_2.png +0 -0
- cfclient/ui/wizards/bslh_3.png +0 -0
- cfclient/ui/wizards/bslh_4.png +0 -0
- cfclient/ui/wizards/bslh_5.png +0 -0
- cfclient/ui/wizards/lighthouse_geo_bs_estimation_wizard.py +465 -0
- cfclient/utils/config_manager.py +5 -4
- cfclient/utils/input/__init__.py +77 -19
- cfclient/utils/input/inputinterfaces/wiimote.py +2 -2
- cfclient/utils/input/inputreaderinterface.py +17 -7
- cfclient/utils/input/inputreaders/__init__.py +17 -0
- cfclient/utils/logconfigreader.py +245 -25
- cfclient/utils/logdatawriter.py +3 -1
- cfclient/utils/periodictimer.py +1 -1
- cfclient/utils/ui.py +336 -0
- cfclient/utils/zmq_led_driver.py +5 -0
- cfclient/utils/zmq_param.py +6 -0
- cfclient/version.py +34 -1
- cfclient-2025.12.1.dist-info/METADATA +70 -0
- cfclient-2025.12.1.dist-info/RECORD +152 -0
- {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/WHEEL +1 -1
- {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/entry_points.txt +0 -1
- cfclient-2025.12.1.dist-info/licenses/LICENSE.txt +350 -0
- {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/top_level.txt +1 -0
- cfconfig/Makefile +51 -0
- cfconfig/configblock.py +111 -0
- cfloader/__init__.py +41 -55
- cfzmq/__init__.py +22 -14
- cfclient/ui/dialogs/cf1config.py +0 -265
- cfclient/ui/dialogs/cf1config.ui +0 -260
- cfclient/ui/tab.py +0 -96
- cfclient/ui/tabs/LEDTab.py +0 -169
- cfclient/ui/toolboxes/ConsoleToolbox.py +0 -69
- cfclient/ui/toolboxes/DebugDriverToolbox.py +0 -107
- cfclient/ui/toolboxes/__init__.py +0 -45
- cfclient/ui/toolboxes/consoleToolbox.ui +0 -62
- cfclient/ui/toolboxes/debugDriverToolbox.ui +0 -86
- cfclient-2017.4.dist-info/DESCRIPTION.rst +0 -3
- cfclient-2017.4.dist-info/METADATA +0 -22
- cfclient-2017.4.dist-info/RECORD +0 -104
- cfclient-2017.4.dist-info/metadata.json +0 -1
- /cfclient/{icon-256.png → ui/icons/icon-256.png} +0 -0
- /cfclient/ui/{toolboxes → tabs}/crtpSharkToolbox.ui +0 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# || ____ _ __
|
|
5
|
+
# +------+ / __ )(_) /_______________ _____ ___
|
|
6
|
+
# | 0xBC | / __ / / __/ ___/ ___/ __ `/_ / / _ \
|
|
7
|
+
# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
|
|
8
|
+
# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
|
|
9
|
+
#
|
|
10
|
+
# Copyright (C) 2011-2025 Bitcraze AB
|
|
11
|
+
#
|
|
12
|
+
# Crazyflie Nano Quadcopter Client
|
|
13
|
+
#
|
|
14
|
+
# This program is free software; you can redistribute it and/or
|
|
15
|
+
# modify it under the terms of the GNU General Public License
|
|
16
|
+
# as published by the Free Software Foundation; either version 2
|
|
17
|
+
# of the License, or (at your option) any later version.
|
|
18
|
+
#
|
|
19
|
+
# This program is distributed in the hope that it will be useful,
|
|
20
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
21
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
22
|
+
# GNU General Public License for more details.
|
|
23
|
+
|
|
24
|
+
# You should have received a copy of the GNU General Public License
|
|
25
|
+
# along with this program; if not, write to the Free Software
|
|
26
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
27
|
+
# 02110-1301, USA.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
Basic tab to be able to set (and test) color in Color LED.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
from PyQt6 import uic
|
|
35
|
+
from PyQt6.QtCore import Qt, pyqtSignal
|
|
36
|
+
from PyQt6.QtGui import QColor, QPixmap, QPainter, QLinearGradient, QPen, QPainterPath
|
|
37
|
+
from PyQt6.QtWidgets import QPushButton, QMessageBox
|
|
38
|
+
from cflib.crazyflie.log import LogConfig
|
|
39
|
+
|
|
40
|
+
import cfclient
|
|
41
|
+
from cfclient.ui.tab_toolbox import TabToolbox
|
|
42
|
+
from cfclient.utils.config import Config
|
|
43
|
+
|
|
44
|
+
__author__ = 'Bitcraze AB'
|
|
45
|
+
__all__ = ['ColorLEDTab']
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
color_led_tab_class = uic.loadUiType(cfclient.module_path + "/ui/tabs/colorLEDTab.ui")[0] # type: ignore
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class wrgb_t:
|
|
53
|
+
def __init__(self, w: int, r: int, g: int, b: int):
|
|
54
|
+
self.w = w
|
|
55
|
+
self.r = r
|
|
56
|
+
self.g = g
|
|
57
|
+
self.b = b
|
|
58
|
+
|
|
59
|
+
def pack(self) -> int:
|
|
60
|
+
"""Pack WRGB values into uint32 format: 0xWWRRGGBB"""
|
|
61
|
+
return (self.w << 24) | (self.r << 16) | (self.g << 8) | self.b
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class rgb_t:
|
|
65
|
+
def __init__(self, r: int, g: int, b: int):
|
|
66
|
+
self.r = r
|
|
67
|
+
self.g = g
|
|
68
|
+
self.b = b
|
|
69
|
+
|
|
70
|
+
def __iter__(self):
|
|
71
|
+
return iter([self.r, self.g, self.b])
|
|
72
|
+
|
|
73
|
+
def extract_white(self) -> wrgb_t:
|
|
74
|
+
"""Extract white channel and return WRGB color"""
|
|
75
|
+
white = min(self.r, self.g, self.b)
|
|
76
|
+
return wrgb_t(white, self.r - white, self.g - white, self.b - white)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ThermalMonitor:
|
|
80
|
+
"""Monitors thermal throttling status for Color LED decks"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, helper, params_config, log_data_signal, log_error_signal):
|
|
83
|
+
"""
|
|
84
|
+
Initialize thermal monitor
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
helper: Crazyflie helper instance
|
|
88
|
+
params_config: Dictionary mapping positions to their thermal log names
|
|
89
|
+
log_data_signal: PyQt signal to emit when log data is received
|
|
90
|
+
log_error_signal: PyQt signal to emit when log errors occur
|
|
91
|
+
"""
|
|
92
|
+
self._helper = helper
|
|
93
|
+
self._params_config = params_config
|
|
94
|
+
self._log_data_signal = log_data_signal
|
|
95
|
+
self._log_error_signal = log_error_signal
|
|
96
|
+
self._active_positions = []
|
|
97
|
+
|
|
98
|
+
def start_monitoring(self, positions):
|
|
99
|
+
"""
|
|
100
|
+
Start thermal monitoring for specified deck positions
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
positions: List of position indices to monitor
|
|
104
|
+
"""
|
|
105
|
+
self._active_positions = positions
|
|
106
|
+
|
|
107
|
+
for position in positions:
|
|
108
|
+
if position not in self._params_config:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
params = self._params_config[position]
|
|
112
|
+
log_name = f"Thermal{['Bottom', 'Top'][position]}"
|
|
113
|
+
lg = LogConfig(log_name, Config().get("ui_update_period"))
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
lg.add_variable(params['thermal_log'], "uint8_t")
|
|
117
|
+
self._helper.cf.log.add_config(lg)
|
|
118
|
+
lg.data_received_cb.add_callback(self._log_data_signal.emit)
|
|
119
|
+
lg.error_cb.add_callback(self._log_error_signal.emit)
|
|
120
|
+
lg.start()
|
|
121
|
+
logger.debug(f"Started thermal logging for position {position}: {params['thermal_log']}")
|
|
122
|
+
except (KeyError, AttributeError) as e:
|
|
123
|
+
logger.debug(f"Could not start thermal logging for position {position}: {e}")
|
|
124
|
+
|
|
125
|
+
def check_throttling(self, data, positions_to_check):
|
|
126
|
+
"""
|
|
127
|
+
Check if any of the specified positions are thermally throttling
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
data: Log data dictionary
|
|
131
|
+
positions_to_check: List of position indices to check
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
bool: True if any position is throttling
|
|
135
|
+
"""
|
|
136
|
+
return any(
|
|
137
|
+
self._params_config[pos]['thermal_log'] in data and
|
|
138
|
+
data[self._params_config[pos]['thermal_log']]
|
|
139
|
+
for pos in positions_to_check
|
|
140
|
+
if pos in self._params_config
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ColorLEDDeckController:
|
|
145
|
+
"""Manages Color LED deck detection and parameter writes"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, helper, params_config):
|
|
148
|
+
"""
|
|
149
|
+
Initialize deck controller
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
helper: Crazyflie helper instance
|
|
153
|
+
params_config: Dictionary mapping positions to their parameter names
|
|
154
|
+
"""
|
|
155
|
+
self._helper = helper
|
|
156
|
+
self._params_config = params_config
|
|
157
|
+
self._deck_present = {} # Maps position -> bool
|
|
158
|
+
|
|
159
|
+
def detect_decks(self):
|
|
160
|
+
"""Detect which Color LED decks are present"""
|
|
161
|
+
for position, params in self._params_config.items():
|
|
162
|
+
try:
|
|
163
|
+
deck_param = self._helper.cf.param.get_value(params['deck_param'])
|
|
164
|
+
# Convert to int to handle string values like "0" or "1"
|
|
165
|
+
self._deck_present[position] = bool(int(deck_param))
|
|
166
|
+
logger.debug(f"Color LED deck at position {position} ({'Bottom' if position == 0 else 'Top'}) "
|
|
167
|
+
f"detected: {self._deck_present[position]} (param: {params['deck_param']}={deck_param})")
|
|
168
|
+
except (KeyError, ValueError, TypeError) as e:
|
|
169
|
+
self._deck_present[position] = False
|
|
170
|
+
logger.debug(f"Color LED deck parameter not found for position {position}: {e}")
|
|
171
|
+
|
|
172
|
+
def is_deck_present(self, position):
|
|
173
|
+
"""Check if deck at given position is present"""
|
|
174
|
+
return self._deck_present.get(position, False)
|
|
175
|
+
|
|
176
|
+
def get_present_decks(self):
|
|
177
|
+
"""Get list of positions where decks are present"""
|
|
178
|
+
return [pos for pos, present in self._deck_present.items() if present]
|
|
179
|
+
|
|
180
|
+
def write_color(self, position, color_uint32):
|
|
181
|
+
"""
|
|
182
|
+
Write color to a specific deck position
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
position: Deck position (0=bottom, 1=top)
|
|
186
|
+
color_uint32: Packed WRGB color value
|
|
187
|
+
"""
|
|
188
|
+
if position not in self._params_config:
|
|
189
|
+
logger.warning(f"Unknown position {position}")
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
if not self.is_deck_present(position):
|
|
193
|
+
logger.debug(f"Color LED deck at position {position} not present, skipping color write")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
param_name = self._params_config[position]['color']
|
|
197
|
+
self._helper.cf.param.set_value(param_name, str(color_uint32))
|
|
198
|
+
|
|
199
|
+
def clear_deck_state(self):
|
|
200
|
+
"""Clear deck presence state (called on disconnect)"""
|
|
201
|
+
self._deck_present.clear()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ColorLEDTab(TabToolbox, color_led_tab_class):
|
|
205
|
+
"""Tab with inline color picker with hue slider, SV area, and hex input."""
|
|
206
|
+
|
|
207
|
+
_colorChanged = pyqtSignal(QColor)
|
|
208
|
+
_connectedSignal = pyqtSignal(str)
|
|
209
|
+
_disconnectedSignal = pyqtSignal(str)
|
|
210
|
+
_log_data_signal = pyqtSignal(int, object, object)
|
|
211
|
+
_log_error_signal = pyqtSignal(object, str)
|
|
212
|
+
|
|
213
|
+
# Parameter and log names by position
|
|
214
|
+
PARAMS_BY_POSITION = {
|
|
215
|
+
0: { # Bottom
|
|
216
|
+
'color': 'colorLedBot.wrgb8888',
|
|
217
|
+
'thermal_log': 'colorLedBot.throttlePct',
|
|
218
|
+
'brightness': 'colorLedBot.brightCorr',
|
|
219
|
+
'deck_param': 'deck.bcColorLedBot'
|
|
220
|
+
},
|
|
221
|
+
1: { # Top
|
|
222
|
+
'color': 'colorLedTop.wrgb8888',
|
|
223
|
+
'thermal_log': 'colorLedTop.throttlePct',
|
|
224
|
+
'brightness': 'colorLedTop.brightCorr',
|
|
225
|
+
'deck_param': 'deck.bcColorLedTop'
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
def __init__(self, helper):
|
|
230
|
+
super(ColorLEDTab, self).__init__(helper, 'Color LED')
|
|
231
|
+
self.setupUi(self)
|
|
232
|
+
|
|
233
|
+
self.groupBox_color.setEnabled(False)
|
|
234
|
+
self.hue_bar.setEnabled(False)
|
|
235
|
+
|
|
236
|
+
self._populate_position_dropdown()
|
|
237
|
+
|
|
238
|
+
self._hue = 0
|
|
239
|
+
self._saturation = 0
|
|
240
|
+
self._value = 0
|
|
241
|
+
|
|
242
|
+
self.hue_bar.setMinimum(0)
|
|
243
|
+
self.hue_bar.setMaximum(1000)
|
|
244
|
+
self.hue_bar.setValue(int(self._hue * 1000))
|
|
245
|
+
self.hue_bar.valueChanged.connect(self._on_hue_slider_changed)
|
|
246
|
+
self.hue_bar.setMouseTracking(True)
|
|
247
|
+
|
|
248
|
+
self.hex_input.editingFinished.connect(self._on_hex_changed)
|
|
249
|
+
|
|
250
|
+
self.custom_color_buttons = []
|
|
251
|
+
self._connect_color_buttons()
|
|
252
|
+
|
|
253
|
+
self.add_color_button.clicked.connect(self._add_custom_color_button)
|
|
254
|
+
|
|
255
|
+
self.sv_area.setMouseTracking(True)
|
|
256
|
+
self.sv_area.setStyleSheet("""
|
|
257
|
+
QLabel {
|
|
258
|
+
border-radius: 8px;
|
|
259
|
+
border: 1px solid #444;
|
|
260
|
+
background-color: #222;
|
|
261
|
+
}
|
|
262
|
+
""")
|
|
263
|
+
|
|
264
|
+
self._isConnected = False
|
|
265
|
+
self._updating_from_fetch = False # Flag to prevent writes during color fetch
|
|
266
|
+
self._throttling_state = {} # Track last known throttling state per position
|
|
267
|
+
|
|
268
|
+
# Initialize thermal monitor and deck controller
|
|
269
|
+
self._thermal_monitor = ThermalMonitor(
|
|
270
|
+
self._helper,
|
|
271
|
+
self.PARAMS_BY_POSITION,
|
|
272
|
+
self._log_data_signal,
|
|
273
|
+
self._log_error_signal
|
|
274
|
+
)
|
|
275
|
+
self._deck_controller = ColorLEDDeckController(
|
|
276
|
+
self._helper,
|
|
277
|
+
self.PARAMS_BY_POSITION
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
self._connectedSignal.connect(self._connected)
|
|
281
|
+
self._disconnectedSignal.connect(self._disconnected)
|
|
282
|
+
self._colorChanged.connect(self._write_color_parameter)
|
|
283
|
+
self._log_data_signal.connect(self._log_data_received)
|
|
284
|
+
self._log_error_signal.connect(self._logging_error)
|
|
285
|
+
|
|
286
|
+
self._helper.cf.connected.add_callback(self._connectedSignal.emit)
|
|
287
|
+
self._helper.cf.disconnected.add_callback(self._disconnectedSignal.emit)
|
|
288
|
+
|
|
289
|
+
# Handle position dropdown changes
|
|
290
|
+
self.positionDropdown.currentIndexChanged.connect(self._on_position_changed)
|
|
291
|
+
|
|
292
|
+
def _logging_error(self, log_conf, msg):
|
|
293
|
+
QMessageBox.about(self, "Log error",
|
|
294
|
+
"Error when starting log config [%s]: %s" % (
|
|
295
|
+
log_conf.name, msg))
|
|
296
|
+
|
|
297
|
+
def _log_data_received(self, _timestamp, data, _logconf):
|
|
298
|
+
if not self.isVisible():
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
position = self.positionDropdown.currentData()
|
|
302
|
+
|
|
303
|
+
# Determine which positions to check for throttling
|
|
304
|
+
positions_to_check = [0, 1] if position == 2 else [position]
|
|
305
|
+
|
|
306
|
+
# Update throttling state for positions that have data in this update
|
|
307
|
+
has_thermal_data = False
|
|
308
|
+
for pos in positions_to_check:
|
|
309
|
+
if pos in self.PARAMS_BY_POSITION:
|
|
310
|
+
thermal_log = self.PARAMS_BY_POSITION[pos]['thermal_log']
|
|
311
|
+
if thermal_log in data:
|
|
312
|
+
has_thermal_data = True
|
|
313
|
+
# Update state for this position
|
|
314
|
+
self._throttling_state[pos] = bool(data[thermal_log])
|
|
315
|
+
|
|
316
|
+
if has_thermal_data:
|
|
317
|
+
# Check if any selected position is currently throttling based on known state
|
|
318
|
+
is_throttling = any(
|
|
319
|
+
self._throttling_state.get(pos, False)
|
|
320
|
+
for pos in positions_to_check
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
self.information_text.setText(
|
|
324
|
+
"Throttling: Lowering intensity to lower temperature." if is_throttling else ""
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _connected(self, _):
|
|
328
|
+
self._isConnected = True
|
|
329
|
+
|
|
330
|
+
# Detect which color LED decks are attached
|
|
331
|
+
self._deck_controller.detect_decks()
|
|
332
|
+
|
|
333
|
+
# Update dropdown based on detected decks
|
|
334
|
+
self._update_position_dropdown()
|
|
335
|
+
|
|
336
|
+
# Enable UI controls only if at least one deck is present
|
|
337
|
+
present_decks = self._deck_controller.get_present_decks()
|
|
338
|
+
has_decks = len(present_decks) > 0
|
|
339
|
+
self.groupBox_color.setEnabled(has_decks)
|
|
340
|
+
self.hue_bar.setEnabled(has_decks)
|
|
341
|
+
|
|
342
|
+
# Set up thermal logging for available decks
|
|
343
|
+
if has_decks:
|
|
344
|
+
self._thermal_monitor.start_monitoring(present_decks)
|
|
345
|
+
|
|
346
|
+
def _disconnected(self, _):
|
|
347
|
+
self._isConnected = False
|
|
348
|
+
self._deck_controller.clear_deck_state()
|
|
349
|
+
self._throttling_state.clear() # Clear throttling state
|
|
350
|
+
|
|
351
|
+
# Disable UI controls
|
|
352
|
+
self.groupBox_color.setEnabled(False)
|
|
353
|
+
self.hue_bar.setEnabled(False)
|
|
354
|
+
|
|
355
|
+
# Disable all position dropdown items
|
|
356
|
+
self.positionDropdown.model().item(0).setEnabled(False)
|
|
357
|
+
self.positionDropdown.model().item(1).setEnabled(False)
|
|
358
|
+
self.positionDropdown.model().item(2).setEnabled(False)
|
|
359
|
+
|
|
360
|
+
self.information_text.setText("") # clear thermal throttling warning
|
|
361
|
+
|
|
362
|
+
def _write_color_parameter(self, color: QColor):
|
|
363
|
+
if not self._isConnected:
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
# Don't write when we're updating UI from a fetch operation
|
|
367
|
+
if self._updating_from_fetch:
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
r, g, b, _ = color.getRgb()
|
|
371
|
+
rgb = rgb_t(r or 0, g or 0, b or 0)
|
|
372
|
+
wrgb = rgb.extract_white()
|
|
373
|
+
color_uint32 = wrgb.pack()
|
|
374
|
+
|
|
375
|
+
position = self.positionDropdown.currentData()
|
|
376
|
+
|
|
377
|
+
# Determine which positions to write to
|
|
378
|
+
positions_to_write = [0, 1] if position == 2 else [position]
|
|
379
|
+
|
|
380
|
+
for pos in positions_to_write:
|
|
381
|
+
self._deck_controller.write_color(pos, color_uint32)
|
|
382
|
+
|
|
383
|
+
def _on_position_changed(self, _):
|
|
384
|
+
"""Handle position dropdown changes by fetching current color from Crazyflie"""
|
|
385
|
+
if not self._isConnected:
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
position = self.positionDropdown.currentData()
|
|
389
|
+
|
|
390
|
+
if position == 2: # Both
|
|
391
|
+
# Fetch both colors and compare
|
|
392
|
+
color_bottom = self._fetch_color_from_position(0)
|
|
393
|
+
color_top = self._fetch_color_from_position(1)
|
|
394
|
+
|
|
395
|
+
if color_bottom is not None and color_top is not None:
|
|
396
|
+
if color_bottom == color_top:
|
|
397
|
+
# Both are the same, show that color
|
|
398
|
+
self._update_ui_from_rgb(color_bottom)
|
|
399
|
+
else:
|
|
400
|
+
# Different colors, show black
|
|
401
|
+
self._update_ui_from_rgb((0, 0, 0))
|
|
402
|
+
else:
|
|
403
|
+
# Could not fetch one or both colors, show black
|
|
404
|
+
self._update_ui_from_rgb((0, 0, 0))
|
|
405
|
+
else: # Bottom (0) or Top (1)
|
|
406
|
+
color = self._fetch_color_from_position(position)
|
|
407
|
+
if color is not None:
|
|
408
|
+
self._update_ui_from_rgb(color)
|
|
409
|
+
else:
|
|
410
|
+
# Could not fetch color, show black
|
|
411
|
+
self._update_ui_from_rgb((0, 0, 0))
|
|
412
|
+
|
|
413
|
+
def _fetch_color_from_position(self, position):
|
|
414
|
+
"""
|
|
415
|
+
Fetch current color from Crazyflie for given position
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
position: 0 for bottom, 1 for top
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
tuple (r, g, b) or None if fetch failed
|
|
422
|
+
"""
|
|
423
|
+
if position not in self.PARAMS_BY_POSITION:
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
if not self._deck_controller.is_deck_present(position):
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
param_name = self.PARAMS_BY_POSITION[position]['color']
|
|
431
|
+
color_uint32 = int(self._helper.cf.param.get_value(param_name))
|
|
432
|
+
|
|
433
|
+
# Unpack WRGB: 0xWWRRGGBB
|
|
434
|
+
w = (color_uint32 >> 24) & 0xFF
|
|
435
|
+
r = (color_uint32 >> 16) & 0xFF
|
|
436
|
+
g = (color_uint32 >> 8) & 0xFF
|
|
437
|
+
b = color_uint32 & 0xFF
|
|
438
|
+
|
|
439
|
+
# Convert WRGB back to RGB by adding white channel back
|
|
440
|
+
r_full = r + w
|
|
441
|
+
g_full = g + w
|
|
442
|
+
b_full = b + w
|
|
443
|
+
|
|
444
|
+
return (r_full, g_full, b_full)
|
|
445
|
+
except (KeyError, ValueError, TypeError) as e:
|
|
446
|
+
logger.debug(f"Could not fetch color from position {position}: {e}")
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
def _update_ui_from_rgb(self, rgb):
|
|
450
|
+
"""
|
|
451
|
+
Update UI controls from RGB values without writing back to Crazyflie
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
rgb: tuple (r, g, b)
|
|
455
|
+
"""
|
|
456
|
+
self._updating_from_fetch = True
|
|
457
|
+
try:
|
|
458
|
+
r, g, b = rgb
|
|
459
|
+
color = QColor(r, g, b)
|
|
460
|
+
h, s, v, _ = color.getHsvF()
|
|
461
|
+
|
|
462
|
+
self._hue = h or 0
|
|
463
|
+
self._saturation = s or 0
|
|
464
|
+
self._value = v or 0
|
|
465
|
+
|
|
466
|
+
self.hue_bar.setValue(int(self._hue * 1000))
|
|
467
|
+
self._update_sv_area(self.sv_area, self._hue)
|
|
468
|
+
self._update_preview()
|
|
469
|
+
finally:
|
|
470
|
+
self._updating_from_fetch = False
|
|
471
|
+
|
|
472
|
+
def _populate_position_dropdown(self):
|
|
473
|
+
"""Initialize the position dropdown with items (called once during __init__)"""
|
|
474
|
+
self.positionDropdown.addItem("Bottom", 0)
|
|
475
|
+
self.positionDropdown.addItem("Top", 1)
|
|
476
|
+
self.positionDropdown.addItem("Both", 2)
|
|
477
|
+
|
|
478
|
+
# Initially all disabled until we connect and detect decks
|
|
479
|
+
self.positionDropdown.model().item(0).setEnabled(False)
|
|
480
|
+
self.positionDropdown.model().item(1).setEnabled(False)
|
|
481
|
+
self.positionDropdown.model().item(2).setEnabled(False)
|
|
482
|
+
|
|
483
|
+
def _update_position_dropdown(self):
|
|
484
|
+
"""Update position dropdown based on detected decks"""
|
|
485
|
+
is_bottom_attached = self._deck_controller.is_deck_present(0)
|
|
486
|
+
is_top_attached = self._deck_controller.is_deck_present(1)
|
|
487
|
+
|
|
488
|
+
if is_bottom_attached and is_top_attached:
|
|
489
|
+
self.positionDropdown.model().item(0).setEnabled(True)
|
|
490
|
+
self.positionDropdown.model().item(1).setEnabled(True)
|
|
491
|
+
self.positionDropdown.model().item(2).setEnabled(True)
|
|
492
|
+
# Default to "Both" if both attached
|
|
493
|
+
self.positionDropdown.setCurrentIndex(2)
|
|
494
|
+
elif is_bottom_attached:
|
|
495
|
+
self.positionDropdown.model().item(0).setEnabled(True)
|
|
496
|
+
self.positionDropdown.model().item(1).setEnabled(False)
|
|
497
|
+
self.positionDropdown.model().item(2).setEnabled(False)
|
|
498
|
+
self.positionDropdown.setCurrentIndex(0)
|
|
499
|
+
elif is_top_attached:
|
|
500
|
+
self.positionDropdown.model().item(0).setEnabled(False)
|
|
501
|
+
self.positionDropdown.model().item(1).setEnabled(True)
|
|
502
|
+
self.positionDropdown.model().item(2).setEnabled(False)
|
|
503
|
+
self.positionDropdown.setCurrentIndex(1)
|
|
504
|
+
else:
|
|
505
|
+
# No decks attached
|
|
506
|
+
self.positionDropdown.model().item(0).setEnabled(False)
|
|
507
|
+
self.positionDropdown.model().item(1).setEnabled(False)
|
|
508
|
+
self.positionDropdown.model().item(2).setEnabled(False)
|
|
509
|
+
|
|
510
|
+
def showEvent(self, a0):
|
|
511
|
+
""" Show event for proper initial SV area sizing """
|
|
512
|
+
super().showEvent(a0)
|
|
513
|
+
self._update_sv_area(self.sv_area, self._hue)
|
|
514
|
+
# Update preview without writing to Crazyflie
|
|
515
|
+
self._updating_from_fetch = True
|
|
516
|
+
try:
|
|
517
|
+
self._update_preview()
|
|
518
|
+
finally:
|
|
519
|
+
self._updating_from_fetch = False
|
|
520
|
+
|
|
521
|
+
def mousePressEvent(self, a0):
|
|
522
|
+
self._handle_mouse_event(a0)
|
|
523
|
+
|
|
524
|
+
def mouseMoveEvent(self, a0):
|
|
525
|
+
self._handle_mouse_event(a0)
|
|
526
|
+
|
|
527
|
+
def _handle_mouse_event(self, event):
|
|
528
|
+
if not self.groupBox_color.isEnabled():
|
|
529
|
+
return
|
|
530
|
+
sv_pos = self.sv_area.mapFrom(self, event.pos())
|
|
531
|
+
if self.sv_area.rect().contains(sv_pos):
|
|
532
|
+
self._update_sv_from_pos(sv_pos)
|
|
533
|
+
|
|
534
|
+
def _update_sv_from_pos(self, pos):
|
|
535
|
+
x = pos.x()
|
|
536
|
+
y = pos.y()
|
|
537
|
+
w = self.sv_area.width()
|
|
538
|
+
h = self.sv_area.height()
|
|
539
|
+
|
|
540
|
+
self._saturation = max(0, min(1, x / w))
|
|
541
|
+
self._value = 1 - max(0, min(1, y / h))
|
|
542
|
+
self._update_preview()
|
|
543
|
+
self._update_sv_area(self.sv_area, self._hue)
|
|
544
|
+
|
|
545
|
+
def _update_sv_area(self, sv_area, hue):
|
|
546
|
+
width = sv_area.width()
|
|
547
|
+
height = sv_area.height()
|
|
548
|
+
if width <= 0 or height <= 0:
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
pixmap = QPixmap(width, height)
|
|
552
|
+
pixmap.fill(Qt.GlobalColor.transparent)
|
|
553
|
+
|
|
554
|
+
painter = QPainter(pixmap)
|
|
555
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
556
|
+
|
|
557
|
+
""" Rounded corners """
|
|
558
|
+
path = QPainterPath()
|
|
559
|
+
corner_radius = 8
|
|
560
|
+
path.addRoundedRect(0, 0, width, height, corner_radius, corner_radius)
|
|
561
|
+
painter.setClipPath(path)
|
|
562
|
+
|
|
563
|
+
""" Base hue layer """
|
|
564
|
+
base_color = QColor.fromHsvF(hue, 1, 1)
|
|
565
|
+
painter.fillRect(pixmap.rect(), base_color)
|
|
566
|
+
|
|
567
|
+
""" Saturation overlay (white → transparent) """
|
|
568
|
+
sat_gradient = QLinearGradient(0, 0, width, 0)
|
|
569
|
+
sat_gradient.setColorAt(0, Qt.GlobalColor.white)
|
|
570
|
+
sat_gradient.setColorAt(1, Qt.GlobalColor.transparent)
|
|
571
|
+
painter.fillRect(pixmap.rect(), sat_gradient)
|
|
572
|
+
|
|
573
|
+
""" Value overlay (transparent → black) """
|
|
574
|
+
val_gradient = QLinearGradient(0, 0, 0, height)
|
|
575
|
+
val_gradient.setColorAt(0, Qt.GlobalColor.transparent)
|
|
576
|
+
val_gradient.setColorAt(1, Qt.GlobalColor.black)
|
|
577
|
+
painter.fillRect(pixmap.rect(), val_gradient)
|
|
578
|
+
|
|
579
|
+
""" Outline """
|
|
580
|
+
outline_pen = QPen(Qt.GlobalColor.darkGray)
|
|
581
|
+
outline_pen.setWidth(1)
|
|
582
|
+
painter.setPen(outline_pen)
|
|
583
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
584
|
+
painter.drawPath(path)
|
|
585
|
+
|
|
586
|
+
""" Circle selector """
|
|
587
|
+
sel_x = int(self._saturation * width)
|
|
588
|
+
sel_y = int((1 - self._value) * height)
|
|
589
|
+
selector_radius = 6
|
|
590
|
+
pen = QPen(Qt.GlobalColor.white)
|
|
591
|
+
pen.setWidth(2)
|
|
592
|
+
painter.setPen(pen)
|
|
593
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
594
|
+
painter.drawEllipse(sel_x - selector_radius, sel_y - selector_radius, selector_radius * 2, selector_radius * 2)
|
|
595
|
+
|
|
596
|
+
painter.end()
|
|
597
|
+
sv_area.setPixmap(pixmap)
|
|
598
|
+
|
|
599
|
+
def _on_hue_slider_changed(self, value):
|
|
600
|
+
if not self.groupBox_color.isEnabled():
|
|
601
|
+
return
|
|
602
|
+
self._hue = value / 1000
|
|
603
|
+
self._update_sv_area(self.sv_area, self._hue)
|
|
604
|
+
self._update_preview()
|
|
605
|
+
|
|
606
|
+
def _update_preview(self):
|
|
607
|
+
color = QColor.fromHsvF(self._hue or 0, self._saturation or 0, self._value or 0)
|
|
608
|
+
self.color_preview.setStyleSheet(f"""
|
|
609
|
+
QFrame#color_preview {{
|
|
610
|
+
border: 1px solid #444;
|
|
611
|
+
border-radius: 4px;
|
|
612
|
+
}}
|
|
613
|
+
|
|
614
|
+
QFrame#color_preview:enabled {{
|
|
615
|
+
background-color: {color.name()};
|
|
616
|
+
}}
|
|
617
|
+
|
|
618
|
+
QFrame#color_preview:disabled {{
|
|
619
|
+
background-color: #777777; /* greyed out when disconnected */
|
|
620
|
+
}}
|
|
621
|
+
""")
|
|
622
|
+
self.hex_input.setText(color.name().upper())
|
|
623
|
+
self._colorChanged.emit(color)
|
|
624
|
+
|
|
625
|
+
def _on_hex_changed(self):
|
|
626
|
+
if not self.groupBox_color.isEnabled():
|
|
627
|
+
return
|
|
628
|
+
hex_value = self.hex_input.text().strip()
|
|
629
|
+
color = QColor(hex_value)
|
|
630
|
+
if color.isValid():
|
|
631
|
+
h, s, v, _ = color.getHsvF()
|
|
632
|
+
self._hue, self._saturation, self._value = h or 0, s or 0, v or 0
|
|
633
|
+
self.hue_bar.setValue(int(self._hue * 1000))
|
|
634
|
+
self._update_sv_area(self.sv_area, self._hue)
|
|
635
|
+
self._update_preview()
|
|
636
|
+
self.hex_input.setStyleSheet("")
|
|
637
|
+
self.hex_error_label.setText("")
|
|
638
|
+
self.information_text.setText("")
|
|
639
|
+
else:
|
|
640
|
+
self.hex_input.setStyleSheet("border: 2px solid red; border-radius: 4px;")
|
|
641
|
+
self.hex_error_label.setText("Invalid hex code.")
|
|
642
|
+
self.information_text.setText("")
|
|
643
|
+
logger.warning(f"Invalid HEX color: {hex_value}")
|
|
644
|
+
|
|
645
|
+
def _connect_color_buttons(self):
|
|
646
|
+
color_buttons = [
|
|
647
|
+
self.color_button1,
|
|
648
|
+
self.color_button2,
|
|
649
|
+
self.color_button3,
|
|
650
|
+
self.color_button4,
|
|
651
|
+
self.color_button5,
|
|
652
|
+
self.color_button6,
|
|
653
|
+
self.color_button7,
|
|
654
|
+
self.color_button8,
|
|
655
|
+
self.color_button9,
|
|
656
|
+
self.color_button10,
|
|
657
|
+
]
|
|
658
|
+
for btn in color_buttons:
|
|
659
|
+
btn.clicked.connect(self._on_color_button_clicked)
|
|
660
|
+
|
|
661
|
+
def _on_color_button_clicked(self):
|
|
662
|
+
if not self.groupBox_color.isEnabled():
|
|
663
|
+
return
|
|
664
|
+
button = self.sender()
|
|
665
|
+
style = button.styleSheet() # type: ignore
|
|
666
|
+
if "background-color:" not in style:
|
|
667
|
+
return
|
|
668
|
+
hex_color = style.split("background-color:")[-1].split(";")[0].strip()
|
|
669
|
+
color = QColor(hex_color)
|
|
670
|
+
if color.isValid():
|
|
671
|
+
h, s, v, _ = color.getHsvF()
|
|
672
|
+
self._hue, self._saturation, self._value = h or 0, s or 0, v or 0
|
|
673
|
+
self.hue_bar.setValue(int(self._hue * 1000))
|
|
674
|
+
self._update_sv_area(self.sv_area, self._hue)
|
|
675
|
+
self._update_preview()
|
|
676
|
+
|
|
677
|
+
def _add_custom_color_button(self):
|
|
678
|
+
color_hex = self.hex_input.text().strip()
|
|
679
|
+
if not color_hex.startswith("#"):
|
|
680
|
+
color_hex = "#" + color_hex
|
|
681
|
+
|
|
682
|
+
color = QColor(color_hex)
|
|
683
|
+
if not color.isValid():
|
|
684
|
+
logger.warning(f"Invalid custom color: {color_hex}")
|
|
685
|
+
return
|
|
686
|
+
|
|
687
|
+
new_btn = QPushButton()
|
|
688
|
+
new_btn.setStyleSheet(f"background-color: {color_hex};")
|
|
689
|
+
new_btn.setFixedSize(50, 30)
|
|
690
|
+
new_btn.clicked.connect(self._on_color_button_clicked)
|
|
691
|
+
new_btn.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
692
|
+
new_btn.customContextMenuRequested.connect(lambda pos, btn=new_btn: self._remove_custom_color_button(btn))
|
|
693
|
+
|
|
694
|
+
self.custom_color_buttons.append(new_btn)
|
|
695
|
+
|
|
696
|
+
self._repack_custom_buttons()
|
|
697
|
+
logger.debug(f"Added new custom color {color_hex}")
|
|
698
|
+
|
|
699
|
+
def _remove_custom_color_button(self, button):
|
|
700
|
+
reply = QMessageBox.question(
|
|
701
|
+
self,
|
|
702
|
+
"Remove Custom Color",
|
|
703
|
+
"Do you want to remove this custom color?",
|
|
704
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
705
|
+
)
|
|
706
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
707
|
+
if button in self.custom_color_buttons:
|
|
708
|
+
self.custom_color_buttons.remove(button)
|
|
709
|
+
button.setParent(None)
|
|
710
|
+
button.deleteLater()
|
|
711
|
+
logger.debug("Removed custom color button.")
|
|
712
|
+
|
|
713
|
+
self._repack_custom_buttons()
|
|
714
|
+
|
|
715
|
+
def _repack_custom_buttons(self):
|
|
716
|
+
grid = self.gridLayout_5
|
|
717
|
+
plus_button = self.add_color_button
|
|
718
|
+
|
|
719
|
+
total_cols = 8
|
|
720
|
+
row, col = 0, 0
|
|
721
|
+
|
|
722
|
+
preset_buttons = [
|
|
723
|
+
self.color_button1,
|
|
724
|
+
self.color_button2,
|
|
725
|
+
self.color_button3,
|
|
726
|
+
self.color_button4,
|
|
727
|
+
self.color_button5,
|
|
728
|
+
self.color_button6,
|
|
729
|
+
self.color_button7,
|
|
730
|
+
self.color_button8,
|
|
731
|
+
self.color_button9,
|
|
732
|
+
self.color_button10,
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
for btn in self.custom_color_buttons + [plus_button]:
|
|
736
|
+
grid.removeWidget(btn)
|
|
737
|
+
|
|
738
|
+
for btn in preset_buttons:
|
|
739
|
+
grid.addWidget(btn, row, col)
|
|
740
|
+
col += 1
|
|
741
|
+
if col >= total_cols:
|
|
742
|
+
col = 0
|
|
743
|
+
row += 1
|
|
744
|
+
|
|
745
|
+
for btn in self.custom_color_buttons:
|
|
746
|
+
grid.addWidget(btn, row, col)
|
|
747
|
+
col += 1
|
|
748
|
+
if col >= total_cols:
|
|
749
|
+
col = 0
|
|
750
|
+
row += 1
|
|
751
|
+
|
|
752
|
+
grid.addWidget(plus_button, row, col)
|