pyscreeps-arena 0.5.7b0__py3-none-any.whl → 0.5.7.2__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.
@@ -0,0 +1,441 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ QPSA Cell Object Component - 单元格对象组件
4
+ """
5
+ from typing import List, Dict, Any, Optional
6
+ import json
7
+ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
8
+ QListWidget, QListWidgetItem, QFrame,
9
+ QSpacerItem, QSizePolicy, QPushButton)
10
+ from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot, QMimeData
11
+ from PyQt6.QtGui import QPainter, QPen, QColor, QBrush, QPixmap, QDrag
12
+
13
+
14
+ class QPSACellObjectItem(QWidget):
15
+ """List item widget with remove button."""
16
+
17
+ # Signals
18
+ removeClicked = pyqtSignal(str) # object_id
19
+
20
+ def __init__(self, obj_id: str, obj_type: str, obj_name: str,
21
+ x: int, y: int, method: str, image: Optional[QPixmap] = None, parent=None):
22
+ super().__init__(parent)
23
+ self._id = obj_id
24
+ self._type = obj_type
25
+ self._name = obj_name
26
+ self._x = x
27
+ self._y = y
28
+ self._method = method
29
+ self._image = image
30
+ self._init_ui()
31
+
32
+ def _init_ui(self):
33
+ """Initialize UI components."""
34
+ # Main horizontal layout
35
+ layout = QHBoxLayout()
36
+ layout.setContentsMargins(2, 2, 2, 2) # Reduced margins
37
+ layout.setSpacing(2) # Reduced spacing
38
+
39
+ # Left side - shape/icon
40
+ self._icon_label = QLabel()
41
+ self._icon_label.setFixedSize(20, 20) # Reduced from 30x30
42
+ self._icon_label.setStyleSheet("""
43
+ QLabel {
44
+ border: 1px solid #ccc;
45
+ border-radius: 3px;
46
+ background-color: #f0f0f0;
47
+ }
48
+ """)
49
+ self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
50
+ self._update_icon()
51
+ layout.addWidget(self._icon_label)
52
+
53
+ # Middle section - vertical layout for type, id and coordinates
54
+ middle_layout = QVBoxLayout()
55
+ middle_layout.setSpacing(1) # Reduced spacing
56
+ middle_layout.setContentsMargins(0, 0, 0, 0)
57
+
58
+ # Type and name label (first row)
59
+ type_name_label = QLabel(f"{self._type}: {self._name}")
60
+ type_name_label.setStyleSheet("font-weight: bold; font-size: 9px;") # Reduced font size
61
+ type_name_label.setWordWrap(False)
62
+ middle_layout.addWidget(type_name_label)
63
+
64
+ # ID label (second row)
65
+ id_label = QLabel(f"ID: {self._id}")
66
+ id_label.setStyleSheet("font-size: 8px; color: #666;") # Reduced font size
67
+ id_label.setWordWrap(False)
68
+ middle_layout.addWidget(id_label)
69
+
70
+ # Coordinates and method label (third row)
71
+ coord_method_label = QLabel(f"({self._x}, {self._y}) | {self._method}")
72
+ coord_method_label.setStyleSheet("font-size: 8px; color: #888;") # Reduced font size
73
+ coord_method_label.setWordWrap(False)
74
+ middle_layout.addWidget(coord_method_label)
75
+
76
+ layout.addLayout(middle_layout)
77
+
78
+ # Add spacer to push button to the right
79
+ layout.addStretch()
80
+
81
+ # Right side - remove button with red circle and cross
82
+ self._remove_btn = QPushButton("✕") # Changed to simpler symbol
83
+ self._remove_btn.setFixedSize(16, 16) # Reduced from 24x24
84
+ self._remove_btn.setStyleSheet("""
85
+ QPushButton {
86
+ border: 1px solid #ff6b6b;
87
+ border-radius: 8px;
88
+ background-color: #ffe6e6;
89
+ color: #ff6b6b;
90
+ font-size: 10px;
91
+ font-weight: bold;
92
+ }
93
+ QPushButton:hover {
94
+ background-color: #ff6b6b;
95
+ color: white;
96
+ }
97
+ QPushButton:pressed {
98
+ background-color: #ff5252;
99
+ }
100
+ """)
101
+ self._remove_btn.clicked.connect(self._on_remove_clicked)
102
+ layout.addWidget(self._remove_btn)
103
+
104
+ self.setLayout(layout)
105
+
106
+ # self.setMinimumWidth(160) # Reduced from 200
107
+
108
+ def _update_icon(self):
109
+ """Update the icon based on current image."""
110
+ if self._image and not self._image.isNull():
111
+ # Get the label size and use 90% of it for the image
112
+ label_width = self._icon_label.width()
113
+ label_height = self._icon_label.height()
114
+
115
+ # Calculate 90% of container size
116
+ target_width = int(label_width * 0.9)
117
+ target_height = int(label_height * 0.9)
118
+
119
+ # Scale image to fit the label with 90% size
120
+ scaled_image = self._image.scaled(
121
+ target_width, target_height, Qt.AspectRatioMode.KeepAspectRatio,
122
+ Qt.TransformationMode.SmoothTransformation
123
+ )
124
+ self._icon_label.setPixmap(scaled_image)
125
+ self._icon_label.setText("")
126
+ else:
127
+ # Use placeholder text if no image
128
+ self._icon_label.setPixmap(QPixmap())
129
+ self._icon_label.setText("◆") # Placeholder shape
130
+
131
+ @pyqtSlot()
132
+ def _on_remove_clicked(self):
133
+ """Handle remove button click."""
134
+ self.removeClicked.emit(self._id)
135
+
136
+ # Properties
137
+ @property
138
+ def id(self) -> str:
139
+ """Get object ID."""
140
+ return self._id
141
+
142
+ @property
143
+ def image(self) -> Optional[QPixmap]:
144
+ """Get object image/screenshot."""
145
+ return self._image
146
+
147
+
148
+ class QPSACellObject(QWidget):
149
+ """Cell object component that accepts drag and drop."""
150
+
151
+ # Signals
152
+ objectAdded = pyqtSignal(object) # Emitted when an object is added
153
+ objectRemoved = pyqtSignal(str) # Emitted when an object is removed
154
+ itemChanged = pyqtSignal() # Emitted when any item is added or removed
155
+
156
+ def __init__(self, parent=None):
157
+ super().__init__(parent)
158
+ self._objects = [] # List to store objects
159
+ self._init_ui()
160
+ self._drag_start_pos = None
161
+ self._dragging_item = None
162
+
163
+ # Install event filter on the list widget to capture mouse events
164
+ self._objects_list.viewport().installEventFilter(self)
165
+
166
+ def _init_ui(self):
167
+ """Initialize UI components."""
168
+ # Main vertical layout
169
+ layout = QVBoxLayout()
170
+ layout.setContentsMargins(2, 2, 2, 2) # Reduced margins
171
+ layout.setSpacing(2) # Reduced spacing
172
+
173
+ # Title label
174
+ # title_label = QLabel("Cell Objects")
175
+ # title_label.setStyleSheet("font-weight: bold; font-size: 14px;")
176
+ # layout.addWidget(title_label)
177
+
178
+ # Objects list
179
+ self._objects_list = QListWidget()
180
+ self._objects_list.setFrameStyle(QFrame.Shape.Box)
181
+ self._objects_list.setStyleSheet("""
182
+ QListWidget {
183
+ border: 1px solid #ccc;
184
+ border-radius: 3px;
185
+ background-color: white;
186
+ }
187
+ QListWidget::item {
188
+ border-bottom: 1px solid #eee;
189
+ }
190
+ QListWidget::item:selected {
191
+ background-color: #e3f2fd;
192
+ }
193
+ """)
194
+
195
+ # Set widget to accept drops
196
+ self.setAcceptDrops(True)
197
+
198
+ # Disable list widget's own drag handling to use our custom implementation
199
+ self._objects_list.setDragEnabled(False) # Disable default drag handling
200
+ self._objects_list.setDropIndicatorShown(True)
201
+ self._objects_list.setDragDropMode(QListWidget.DragDropMode.NoDragDrop) # No default drag drop
202
+ self._objects_list.setAcceptDrops(False)
203
+
204
+ layout.addWidget(self._objects_list)
205
+
206
+ self.setLayout(layout)
207
+
208
+ # Set minimum size - reduced from original
209
+ self.setMinimumWidth(175) # Reduced from 200
210
+ self.setMaximumWidth(175) # Reduced from 300
211
+ self.setMinimumHeight(80) # Reduced from 150
212
+ self.setMaximumHeight(120) # Reduced from 150
213
+
214
+ # Drag and drop functionality
215
+ def dragEnterEvent(self, event):
216
+ """Handle drag enter event."""
217
+ print(f"[DEBUG] Drag enter: hasText={event.mimeData().hasText()}, hasJSON={event.mimeData().hasFormat('application/json')}")
218
+ if event.mimeData().hasText() or event.mimeData().hasFormat("application/json"):
219
+ event.acceptProposedAction()
220
+
221
+ def dragMoveEvent(self, event):
222
+ """Handle drag move event."""
223
+ event.acceptProposedAction()
224
+
225
+ def dropEvent(self, event):
226
+ """Handle drop event to add object."""
227
+ mime_data = event.mimeData()
228
+ json_data = None
229
+ image = None
230
+
231
+ print(f"[DEBUG] Drop received: hasText={event.mimeData().hasText()}, hasJSON={event.mimeData().hasFormat('application/json')}, hasImage={event.mimeData().hasImage()}")
232
+
233
+ # Try to get JSON data from different sources
234
+ if mime_data.hasFormat("application/json"):
235
+ try:
236
+ json_bytes = mime_data.data("application/json")
237
+ json_str = json_bytes.data().decode('utf-8')
238
+ print(f"[DEBUG] JSON data: {json_str}")
239
+ json_data = json.loads(json_str)
240
+ except Exception as e:
241
+ print(f"[DEBUG] Failed to parse JSON data: {e}")
242
+ elif mime_data.hasText():
243
+ try:
244
+ json_str = mime_data.text()
245
+ print(f"[DEBUG] Text data: {json_str}")
246
+ json_data = json.loads(json_str)
247
+ except Exception as e:
248
+ print(f"[DEBUG] Failed to parse text as JSON: {e}")
249
+
250
+ # Try to get image data if available
251
+ if mime_data.hasImage():
252
+ try:
253
+ image = QPixmap(mime_data.imageData())
254
+ print(f"[DEBUG] Image data received: {image.size()}")
255
+ except Exception as e:
256
+ print(f"[DEBUG] Failed to get image data: {e}")
257
+
258
+ if json_data:
259
+ # Handle both single object and array of objects
260
+ if isinstance(json_data, list):
261
+ # Array format: [{x, y, type, method, id, name}, ...]
262
+ print(f"[DEBUG] Received array of {len(json_data)} objects")
263
+ for obj in json_data:
264
+ self.add_object(obj, image)
265
+ event.acceptProposedAction()
266
+ print(f"[DEBUG] Array of objects added successfully")
267
+ elif isinstance(json_data, dict):
268
+ # Single object format: {x, y, type, method, id, name}
269
+ self.add_object(json_data, image)
270
+ event.acceptProposedAction()
271
+ print(f"[DEBUG] Single object added successfully: {json_data}")
272
+ else:
273
+ print(f"[DEBUG] Invalid JSON format: expected dict or list, got {type(json_data)}")
274
+ else:
275
+ print(f"[DEBUG] No valid JSON data found")
276
+
277
+ def add_object(self, obj_data: Dict[str, Any], image: Optional[QPixmap] = None):
278
+ """Add an object to the list, rejecting duplicates with same x, y, type."""
279
+ # Extract required fields
280
+ obj_id = obj_data.get('id', 'unknown')
281
+ obj_type = obj_data.get('type', 'Unknown')
282
+ obj_name = obj_data.get('name', obj_id)
283
+ x = obj_data.get('x', 0)
284
+ y = obj_data.get('y', 0)
285
+ method = obj_data.get('method', '')
286
+
287
+ # Check for duplicates with same x, y, type
288
+ for existing_obj in self._objects:
289
+ existing_x = existing_obj.get('x', 0)
290
+ existing_y = existing_obj.get('y', 0)
291
+ existing_type = existing_obj.get('type', 'Unknown')
292
+ if existing_x == x and existing_y == y and existing_type == obj_type:
293
+ print(f"[DEBUG] Rejected duplicate object: {obj_type} at ({x}, {y})")
294
+ return # Reject duplicate
295
+
296
+ # Create custom widget for the object
297
+ item_widget = QPSACellObjectItem(
298
+ obj_id, obj_type, obj_name, x, y, method, image
299
+ )
300
+ item_widget.removeClicked.connect(self._on_remove_clicked)
301
+
302
+ # Create list widget item and set our custom widget
303
+ list_item = QListWidgetItem()
304
+ list_item.setSizeHint(item_widget.sizeHint())
305
+
306
+ self._objects_list.addItem(list_item)
307
+ self._objects_list.setItemWidget(list_item, item_widget)
308
+
309
+ # Add to internal list
310
+ self._objects.append(obj_data)
311
+
312
+ # Emit signals
313
+ self.objectAdded.emit(obj_data)
314
+ self.itemChanged.emit()
315
+ print(f"[DEBUG] Added object: {obj_type} at ({x}, {y})")
316
+
317
+ @pyqtSlot(str)
318
+ def _on_remove_clicked(self, obj_id: str):
319
+ """Handle remove button click."""
320
+ # Find and remove the item from the list
321
+ for i in range(self._objects_list.count()):
322
+ list_item = self._objects_list.item(i)
323
+ widget = self._objects_list.itemWidget(list_item)
324
+ if isinstance(widget, QPSACellObjectItem) and widget.id == obj_id:
325
+ # Remove from internal list
326
+ for obj in self._objects:
327
+ if obj.get('id') == obj_id:
328
+ self._objects.remove(obj)
329
+ break
330
+
331
+ # Remove from UI
332
+ self._objects_list.takeItem(i)
333
+
334
+ # Emit signals
335
+ self.objectRemoved.emit(obj_id)
336
+ self.itemChanged.emit()
337
+ break
338
+
339
+ # Public properties
340
+ @property
341
+ def objects(self) -> List[Dict[str, Any]]:
342
+ """Get all objects."""
343
+ return self._objects.copy()
344
+
345
+ def clear_objects(self):
346
+ """Clear all objects."""
347
+ self._objects_list.clear()
348
+ self._objects.clear()
349
+
350
+ def remove_object(self, obj_id: str):
351
+ """Remove an object by ID."""
352
+ self._on_remove_clicked(obj_id)
353
+
354
+ def set_list_background_color(self, color: str):
355
+ """Set the background color of the internal list widget."""
356
+ # Use triple quotes for multi-line f-string
357
+ style_sheet = f"""
358
+ QListWidget {{
359
+ border: 1px solid #ccc;
360
+ border-radius: 4px;
361
+ background-color: {color};
362
+ }}
363
+ QListWidget::item {{
364
+ border-bottom: 1px solid #eee;
365
+ }}
366
+ QListWidget::item:selected {{
367
+ background-color: #e3f2fd;
368
+ }}
369
+ """
370
+ self._objects_list.setStyleSheet(style_sheet)
371
+
372
+ def eventFilter(self, obj, event):
373
+ """Handle mouse events for the list widget viewport."""
374
+ if obj == self._objects_list.viewport():
375
+ if event.type() == event.Type.MouseButtonPress:
376
+ if event.button() == Qt.MouseButton.LeftButton:
377
+ # Store drag start position and item
378
+ self._drag_start_pos = event.pos()
379
+ # Get the item under the mouse
380
+ item = self._objects_list.itemAt(event.pos())
381
+ self._dragging_item = item
382
+ elif event.type() == event.Type.MouseMove:
383
+ if event.buttons() & Qt.MouseButton.LeftButton:
384
+ if self._drag_start_pos is not None and self._dragging_item is not None:
385
+ # Calculate drag distance
386
+ drag_distance = (event.pos() - self._drag_start_pos).manhattanLength()
387
+ if drag_distance > 20: # Minimum drag distance
388
+ widget = self._objects_list.itemWidget(self._dragging_item)
389
+ if isinstance(widget, QPSACellObjectItem):
390
+ # Find the corresponding object data
391
+ for obj_data in self._objects:
392
+ if obj_data.get('id') == widget.id:
393
+ self._initiate_drag(obj_data, widget.image)
394
+ # Reset drag state
395
+ self._drag_start_pos = None
396
+ self._dragging_item = None
397
+ break
398
+ elif event.type() == event.Type.MouseButtonRelease:
399
+ # Reset drag state
400
+ self._drag_start_pos = None
401
+ self._dragging_item = None
402
+ return False # Let the event propagate
403
+
404
+ def _initiate_drag(self, obj_data: Dict[str, Any], image: Optional[QPixmap]):
405
+ """Initiate drag operation with object data and image."""
406
+ print(f"[DEBUG] Initiating drag for object: {obj_data.get('type')} at ({obj_data.get('x')}, {obj_data.get('y')})")
407
+
408
+ # Create JSON data with required fields
409
+ drag_json = {
410
+ 'x': obj_data.get('x', 0),
411
+ 'y': obj_data.get('y', 0),
412
+ 'id': obj_data.get('id', 'unknown'),
413
+ 'name': obj_data.get('name', obj_data.get('id', 'unknown')),
414
+ 'method': obj_data.get('method', ''),
415
+ 'type': obj_data.get('type', 'Unknown')
416
+ }
417
+
418
+ # Convert to JSON string
419
+ json_str = json.dumps(drag_json, ensure_ascii=False)
420
+ print(f"[DEBUG] Drag JSON: {json_str}")
421
+
422
+ # Create MIME data
423
+ mime_data = QMimeData()
424
+ mime_data.setText(json_str)
425
+ mime_data.setData('application/json', json_str.encode('utf-8'))
426
+
427
+ # Add image if available
428
+ if image and not image.isNull():
429
+ mime_data.setImageData(image)
430
+ print(f"[DEBUG] Adding image to drag: {image.size()}")
431
+
432
+ # Create drag object
433
+ drag = QDrag(self)
434
+ drag.setMimeData(mime_data)
435
+
436
+ # Set a simple drag cursor
437
+ # You could set a custom pixmap here if needed
438
+
439
+ # Start drag operation
440
+ drag.exec(Qt.DropAction.CopyAction)
441
+ print(f"[DEBUG] Drag completed")