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.
- pyscreeps_arena/__init__.py +35 -3
- pyscreeps_arena/core/const.py +1 -1
- pyscreeps_arena/project.7z +0 -0
- pyscreeps_arena/ui/__init__.py +3 -1
- pyscreeps_arena/ui/map_render.py +705 -0
- pyscreeps_arena/ui/mapviewer.py +14 -0
- pyscreeps_arena/ui/qcreeplogic/qcreeplogic.py +82 -21
- pyscreeps_arena/ui/qmapv/__init__.py +3 -0
- pyscreeps_arena/ui/qmapv/qcinfo.py +567 -0
- pyscreeps_arena/ui/qmapv/qco.py +441 -0
- pyscreeps_arena/ui/qmapv/qmapv.py +728 -0
- pyscreeps_arena/ui/qmapv/test_array_drag.py +191 -0
- pyscreeps_arena/ui/qmapv/test_drag.py +107 -0
- pyscreeps_arena/ui/qmapv/test_qcinfo.py +169 -0
- pyscreeps_arena/ui/qmapv/test_qco_drag.py +7 -0
- pyscreeps_arena/ui/qmapv/test_qmapv.py +224 -0
- pyscreeps_arena/ui/qmapv/test_simple_array.py +303 -0
- {pyscreeps_arena-0.5.7b0.dist-info → pyscreeps_arena-0.5.7.2.dist-info}/METADATA +1 -1
- pyscreeps_arena-0.5.7.2.dist-info/RECORD +40 -0
- pyscreeps_arena-0.5.7b0.dist-info/RECORD +0 -28
- {pyscreeps_arena-0.5.7b0.dist-info → pyscreeps_arena-0.5.7.2.dist-info}/WHEEL +0 -0
- {pyscreeps_arena-0.5.7b0.dist-info → pyscreeps_arena-0.5.7.2.dist-info}/entry_points.txt +0 -0
- {pyscreeps_arena-0.5.7b0.dist-info → pyscreeps_arena-0.5.7.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
QPSA Map Viewer - 地图查看器组件
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from typing import Optional, List, Dict, Any
|
|
9
|
+
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
10
|
+
QPushButton, QFileDialog, QGraphicsView,
|
|
11
|
+
QGraphicsScene, QGraphicsPixmapItem, QFrame)
|
|
12
|
+
from PyQt6.QtCore import Qt, pyqtSignal, QPointF, QRectF, QMimeData
|
|
13
|
+
from PyQt6.QtGui import QPixmap, QImage, QPainter, QPen, QColor, QBrush, QDrag
|
|
14
|
+
from PIL import Image, ImageDraw
|
|
15
|
+
import math
|
|
16
|
+
|
|
17
|
+
# Import configuration from build.py
|
|
18
|
+
import sys
|
|
19
|
+
import os
|
|
20
|
+
# Add the project root directory to Python path
|
|
21
|
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
22
|
+
from pyscreeps_arena import config
|
|
23
|
+
|
|
24
|
+
# Language mapping
|
|
25
|
+
LANG = {
|
|
26
|
+
'cn': {
|
|
27
|
+
'no_map_preview': '没有地图可预览,请选择',
|
|
28
|
+
'open_map': '打开地图',
|
|
29
|
+
'map_file_filter': 'JSON文件 (*.json)',
|
|
30
|
+
'all_files': '所有文件 (*)',
|
|
31
|
+
'error': '错误',
|
|
32
|
+
'invalid_json': '无效的JSON文件',
|
|
33
|
+
'load_error': '加载地图失败',
|
|
34
|
+
'file_not_found': '文件未找到',
|
|
35
|
+
'invalid_map_data': '无效的地图数据',
|
|
36
|
+
},
|
|
37
|
+
'en': {
|
|
38
|
+
'no_map_preview': 'No map to preview, please select',
|
|
39
|
+
'open_map': 'Open Map',
|
|
40
|
+
'map_file_filter': 'JSON Files (*.json)',
|
|
41
|
+
'all_files': 'All Files (*)',
|
|
42
|
+
'error': 'Error',
|
|
43
|
+
'invalid_json': 'Invalid JSON file',
|
|
44
|
+
'load_error': 'Failed to load map',
|
|
45
|
+
'file_not_found': 'File not found',
|
|
46
|
+
'invalid_map_data': 'Invalid map data',
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def lang(key: str) -> str:
|
|
51
|
+
"""Helper function to get translated text"""
|
|
52
|
+
return LANG[config.language if hasattr(config, 'language') and config.language in LANG else 'cn'][key]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CellInfo:
|
|
56
|
+
"""Cell information container."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, x: int, y: int):
|
|
59
|
+
self._x = x
|
|
60
|
+
self._y = y
|
|
61
|
+
self._objects: List[Dict[str, Any]] = []
|
|
62
|
+
self._terrain: str = '2' # Default to plain terrain
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def x(self) -> int:
|
|
66
|
+
return self._x
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def y(self) -> int:
|
|
70
|
+
return self._y
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def objects(self) -> List[Dict[str, Any]]:
|
|
74
|
+
return self._objects.copy()
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def terrain(self) -> str:
|
|
78
|
+
return self._terrain
|
|
79
|
+
|
|
80
|
+
@terrain.setter
|
|
81
|
+
def terrain(self, value: str):
|
|
82
|
+
self._terrain = value
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def cost(self) -> int:
|
|
86
|
+
"""
|
|
87
|
+
Get movement cost for this cell.
|
|
88
|
+
|
|
89
|
+
:return: int Movement cost (1=plain+road, 2=plain/swamp+road, 10=swamp, 255=wall)
|
|
90
|
+
"""
|
|
91
|
+
# Check if there's a road in this cell
|
|
92
|
+
has_road = any(obj.get('type') == 'StructureRoad' for obj in self._objects)
|
|
93
|
+
|
|
94
|
+
# Base terrain cost
|
|
95
|
+
if self._terrain == '2': # plain
|
|
96
|
+
base_cost = 2
|
|
97
|
+
elif self._terrain == 'A': # swamp
|
|
98
|
+
base_cost = 10
|
|
99
|
+
elif self._terrain == 'X': # wall
|
|
100
|
+
base_cost = 255
|
|
101
|
+
else:
|
|
102
|
+
base_cost = 2 # Default to plain cost
|
|
103
|
+
|
|
104
|
+
# Apply road reduction
|
|
105
|
+
if has_road:
|
|
106
|
+
if self._terrain == '2': # plain + road
|
|
107
|
+
return 1
|
|
108
|
+
elif self._terrain == 'A': # swamp + road
|
|
109
|
+
return 2
|
|
110
|
+
|
|
111
|
+
return base_cost
|
|
112
|
+
|
|
113
|
+
def add_object(self, obj: Dict[str, Any]):
|
|
114
|
+
"""Add an object to this cell."""
|
|
115
|
+
self._objects.append(obj)
|
|
116
|
+
|
|
117
|
+
def clear_objects(self):
|
|
118
|
+
"""Clear all objects from this cell."""
|
|
119
|
+
self._objects.clear()
|
|
120
|
+
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
return f"CellInfo(x={self._x}, y={self._y}, objects={len(self._objects)})"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class QPSAMapViewer(QWidget):
|
|
126
|
+
"""PyScreeps Arena Map Viewer component."""
|
|
127
|
+
|
|
128
|
+
# Signals
|
|
129
|
+
currentChanged = pyqtSignal(object) # Current cell under mouse changed (CellInfo or None)
|
|
130
|
+
selectChanged = pyqtSignal(object) # Selected cell changed (CellInfo or None)
|
|
131
|
+
rightClicked = pyqtSignal(object) # Right-clicked cell (CellInfo or None)
|
|
132
|
+
|
|
133
|
+
def __init__(self, parent=None):
|
|
134
|
+
super().__init__(parent)
|
|
135
|
+
self._map_data: Optional[Dict[str, Any]] = None
|
|
136
|
+
self._map_path: Optional[str] = None
|
|
137
|
+
self._cell_size = 64 # Default cell size changed to 32
|
|
138
|
+
self._current_cell: Optional[CellInfo] = None
|
|
139
|
+
self._selected_cell: Optional[CellInfo] = None
|
|
140
|
+
self._map_width = 0
|
|
141
|
+
self._map_height = 0
|
|
142
|
+
self._cell_info_grid: List[List[CellInfo]] = []
|
|
143
|
+
self._temp_image_path: Optional[str] = None
|
|
144
|
+
|
|
145
|
+
# Drag functionality
|
|
146
|
+
self._drag_start_pos = None
|
|
147
|
+
self._drag_cell_info = None
|
|
148
|
+
self._original_drag_mode = None
|
|
149
|
+
self._is_dragging = False
|
|
150
|
+
|
|
151
|
+
# Highlight colors
|
|
152
|
+
self._hover_color = QColor(240, 248, 255, 64) # #F0F8FF 25% transparent
|
|
153
|
+
self._selection_width = 2
|
|
154
|
+
|
|
155
|
+
self._init_ui()
|
|
156
|
+
self._init_scene()
|
|
157
|
+
|
|
158
|
+
def _init_ui(self):
|
|
159
|
+
"""Initialize the UI components."""
|
|
160
|
+
# Set minimum size to 1024x1024 and allow expansion
|
|
161
|
+
self.setMinimumSize(1024, 1024)
|
|
162
|
+
self.setSizePolicy(
|
|
163
|
+
self.sizePolicy().Policy.Expanding,
|
|
164
|
+
self.sizePolicy().Policy.Expanding
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Main layout
|
|
168
|
+
layout = QVBoxLayout()
|
|
169
|
+
layout.setContentsMargins(10, 10, 10, 10)
|
|
170
|
+
|
|
171
|
+
# Status label
|
|
172
|
+
self._status_label = QLabel(lang('no_map_preview'))
|
|
173
|
+
self._status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
174
|
+
self._status_label.setStyleSheet("font-size: 14px; color: #666;")
|
|
175
|
+
layout.addWidget(self._status_label)
|
|
176
|
+
|
|
177
|
+
# Graphics view for map display
|
|
178
|
+
self._graphics_view = QGraphicsView()
|
|
179
|
+
self._graphics_view.setFrameShape(QFrame.Shape.NoFrame)
|
|
180
|
+
self._graphics_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
181
|
+
self._graphics_view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
182
|
+
self._graphics_view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
183
|
+
self._graphics_view.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
|
184
|
+
self._graphics_view.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
|
185
|
+
layout.addWidget(self._graphics_view)
|
|
186
|
+
|
|
187
|
+
# Button layout
|
|
188
|
+
button_layout = QHBoxLayout()
|
|
189
|
+
button_layout.addStretch()
|
|
190
|
+
|
|
191
|
+
self._open_button = QPushButton(lang('open_map'))
|
|
192
|
+
self._open_button.setFixedSize(120, 40)
|
|
193
|
+
self._open_button.clicked.connect(self._open_map_file)
|
|
194
|
+
button_layout.addWidget(self._open_button)
|
|
195
|
+
|
|
196
|
+
button_layout.addStretch()
|
|
197
|
+
layout.addLayout(button_layout)
|
|
198
|
+
|
|
199
|
+
self.setLayout(layout)
|
|
200
|
+
|
|
201
|
+
# Initially hide graphics view until map is loaded
|
|
202
|
+
self._graphics_view.hide()
|
|
203
|
+
|
|
204
|
+
def _init_scene(self):
|
|
205
|
+
"""Initialize the graphics scene."""
|
|
206
|
+
self._scene = QGraphicsScene()
|
|
207
|
+
self._graphics_view.setScene(self._scene)
|
|
208
|
+
self._map_item: Optional[QGraphicsPixmapItem] = None
|
|
209
|
+
self._highlight_item: Optional[QGraphicsPixmapItem] = None
|
|
210
|
+
self._selection_item: Optional[QGraphicsPixmapItem] = None
|
|
211
|
+
|
|
212
|
+
def _open_map_file(self):
|
|
213
|
+
"""Open and load a map JSON file."""
|
|
214
|
+
# Set default directory to desktop
|
|
215
|
+
import os
|
|
216
|
+
desktop_path = os.path.join(os.path.expanduser('~'), 'Desktop')
|
|
217
|
+
|
|
218
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
219
|
+
self,
|
|
220
|
+
"选择地图文件",
|
|
221
|
+
desktop_path,
|
|
222
|
+
f"{lang('map_file_filter')};;{lang('all_files')}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if file_path:
|
|
226
|
+
# Change button text to "render..." while rendering
|
|
227
|
+
self._open_button.setText("render...")
|
|
228
|
+
# Force UI update
|
|
229
|
+
from PyQt6.QtWidgets import QApplication
|
|
230
|
+
QApplication.processEvents()
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
self.load_map(file_path)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
self._show_error(f"{lang('load_error')}: {str(e)}")
|
|
236
|
+
# Restore original button text on error
|
|
237
|
+
self._open_button.setText(lang('open_map'))
|
|
238
|
+
|
|
239
|
+
def load_map(self, file_path: str):
|
|
240
|
+
"""
|
|
241
|
+
Load a map from JSON file.
|
|
242
|
+
|
|
243
|
+
:param file_path: Path to the map JSON file
|
|
244
|
+
:raises: ValueError if the file format is invalid
|
|
245
|
+
"""
|
|
246
|
+
if not os.path.exists(file_path):
|
|
247
|
+
raise FileNotFoundError(f"{lang('file_not_found')}: {file_path}")
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
251
|
+
map_data = json.load(f)
|
|
252
|
+
except json.JSONDecodeError as e:
|
|
253
|
+
raise ValueError(f"{lang('invalid_json')}: {e}")
|
|
254
|
+
|
|
255
|
+
# Validate map format
|
|
256
|
+
if 'map' not in map_data or not isinstance(map_data['map'], list):
|
|
257
|
+
raise ValueError(lang('invalid_map_data'))
|
|
258
|
+
|
|
259
|
+
if not map_data['map']:
|
|
260
|
+
raise ValueError(lang('invalid_map_data'))
|
|
261
|
+
|
|
262
|
+
# Validate that all rows have the same length
|
|
263
|
+
row_length = len(map_data['map'][0])
|
|
264
|
+
for i, row in enumerate(map_data['map']):
|
|
265
|
+
if len(row) != row_length:
|
|
266
|
+
raise ValueError(f"{lang('invalid_map_data')}: row {i+1}")
|
|
267
|
+
|
|
268
|
+
self._map_data = map_data
|
|
269
|
+
self._map_path = file_path
|
|
270
|
+
self._map_height = len(map_data['map'])
|
|
271
|
+
self._map_width = row_length
|
|
272
|
+
|
|
273
|
+
# Build cell info grid
|
|
274
|
+
self._build_cell_info_grid()
|
|
275
|
+
|
|
276
|
+
# Render the map
|
|
277
|
+
self._render_map()
|
|
278
|
+
|
|
279
|
+
# Update UI
|
|
280
|
+
self._update_ui_after_load()
|
|
281
|
+
|
|
282
|
+
print(f"[DEBUG] Map loaded: {self._map_width}x{self._map_height}") # 调试输出
|
|
283
|
+
|
|
284
|
+
def _build_cell_info_grid(self):
|
|
285
|
+
"""Build cell information grid from map data."""
|
|
286
|
+
self._cell_info_grid = []
|
|
287
|
+
|
|
288
|
+
# Get terrain data from map
|
|
289
|
+
terrain_map = self._map_data.get('map', [])
|
|
290
|
+
|
|
291
|
+
for y in range(self._map_height):
|
|
292
|
+
row = []
|
|
293
|
+
for x in range(self._map_width):
|
|
294
|
+
cell_info = CellInfo(x, y)
|
|
295
|
+
|
|
296
|
+
# Set terrain if available
|
|
297
|
+
if y < len(terrain_map) and x < len(terrain_map[y]):
|
|
298
|
+
cell_info.terrain = terrain_map[y][x]
|
|
299
|
+
|
|
300
|
+
row.append(cell_info)
|
|
301
|
+
self._cell_info_grid.append(row)
|
|
302
|
+
|
|
303
|
+
# Populate objects from map data
|
|
304
|
+
if 'objects' in self._map_data:
|
|
305
|
+
for obj_type, obj_list in self._map_data['objects'].items():
|
|
306
|
+
# Skip GameObject and Structure base types, but keep their subclasses
|
|
307
|
+
if obj_type in ['GameObject', 'Structure']:
|
|
308
|
+
print(f"[DEBUG] Skipping base type: {obj_type}") # 调试输出
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
for obj in obj_list:
|
|
312
|
+
x, y = obj['x'], obj['y']
|
|
313
|
+
if 0 <= x < self._map_width and 0 <= y < self._map_height:
|
|
314
|
+
obj_copy = obj.copy()
|
|
315
|
+
obj_copy['type'] = obj_type
|
|
316
|
+
self._cell_info_grid[y][x].add_object(obj_copy)
|
|
317
|
+
|
|
318
|
+
def _render_map(self):
|
|
319
|
+
"""Render the map using the existing map_render functionality."""
|
|
320
|
+
try:
|
|
321
|
+
# Import the renderer from the existing module
|
|
322
|
+
from ..map_render import MapRender
|
|
323
|
+
|
|
324
|
+
# Create a temporary file for the rendered image
|
|
325
|
+
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
|
326
|
+
self._temp_image_path = tmp.name
|
|
327
|
+
|
|
328
|
+
# Render the map
|
|
329
|
+
renderer = MapRender(self._map_path, self._cell_size)
|
|
330
|
+
renderer.render(self._temp_image_path, show_grid=True)
|
|
331
|
+
|
|
332
|
+
# Load the rendered image
|
|
333
|
+
pixmap = QPixmap(self._temp_image_path)
|
|
334
|
+
|
|
335
|
+
# Clear existing items
|
|
336
|
+
self._scene.clear()
|
|
337
|
+
self._map_item = None
|
|
338
|
+
self._highlight_item = None
|
|
339
|
+
self._selection_item = None
|
|
340
|
+
|
|
341
|
+
# Add map to scene
|
|
342
|
+
self._map_item = self._scene.addPixmap(pixmap)
|
|
343
|
+
|
|
344
|
+
# Set scene rect to match image size
|
|
345
|
+
self._scene.setSceneRect(QRectF(pixmap.rect()))
|
|
346
|
+
|
|
347
|
+
# Fit the view to the image
|
|
348
|
+
self._graphics_view.fitInView(self._scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
|
|
349
|
+
|
|
350
|
+
# Set default zoom level to 2.0
|
|
351
|
+
self._graphics_view.scale(2.0, 2.0)
|
|
352
|
+
|
|
353
|
+
print(f"[DEBUG] Map rendered to temporary file: {self._temp_image_path}") # 调试输出
|
|
354
|
+
|
|
355
|
+
except Exception as e:
|
|
356
|
+
raise RuntimeError(f"地图渲染失败: {e}")
|
|
357
|
+
|
|
358
|
+
def _update_ui_after_load(self):
|
|
359
|
+
"""Update UI after successful map load."""
|
|
360
|
+
self._status_label.setText(f"地图已加载: {self._map_width}x{self._map_height}")
|
|
361
|
+
self._status_label.hide()
|
|
362
|
+
self._graphics_view.show()
|
|
363
|
+
|
|
364
|
+
# Restore original button text after successful loading
|
|
365
|
+
self._open_button.setText(lang('open_map'))
|
|
366
|
+
|
|
367
|
+
# Enable mouse tracking for hover effects
|
|
368
|
+
self._graphics_view.setMouseTracking(True)
|
|
369
|
+
self._graphics_view.viewport().installEventFilter(self)
|
|
370
|
+
|
|
371
|
+
# Store current map path for language switching
|
|
372
|
+
self._current_map_path = self._map_path
|
|
373
|
+
|
|
374
|
+
# Enable key event handling
|
|
375
|
+
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
376
|
+
|
|
377
|
+
# Enable drag and drop for the graphics view viewport
|
|
378
|
+
self._graphics_view.viewport().setAcceptDrops(True)
|
|
379
|
+
|
|
380
|
+
def dragEnterEvent(self, event):
|
|
381
|
+
"""Accept drag enter events."""
|
|
382
|
+
print(f"[DEBUG] Drag enter event received")
|
|
383
|
+
event.acceptProposedAction()
|
|
384
|
+
|
|
385
|
+
def dragMoveEvent(self, event):
|
|
386
|
+
"""Accept drag move events."""
|
|
387
|
+
event.acceptProposedAction()
|
|
388
|
+
|
|
389
|
+
def dropEvent(self, event):
|
|
390
|
+
"""Handle drop events."""
|
|
391
|
+
print(f"[DEBUG] Drop event received")
|
|
392
|
+
event.acceptProposedAction()
|
|
393
|
+
|
|
394
|
+
def keyPressEvent(self, event):
|
|
395
|
+
"""Handle key press events for Ctrl/Alt drag functionality."""
|
|
396
|
+
if event.key() in (Qt.Key.Key_Control, Qt.Key.Key_Alt):
|
|
397
|
+
if self._original_drag_mode is None:
|
|
398
|
+
# Store original drag mode and disable it
|
|
399
|
+
self._original_drag_mode = self._graphics_view.dragMode()
|
|
400
|
+
self._graphics_view.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
401
|
+
print(f"[DEBUG] Ctrl/Alt pressed: disabled graphics view drag mode (was: {self._original_drag_mode})")
|
|
402
|
+
super().keyPressEvent(event)
|
|
403
|
+
|
|
404
|
+
def keyReleaseEvent(self, event):
|
|
405
|
+
"""Handle key release events to restore drag functionality."""
|
|
406
|
+
if event.key() in (Qt.Key.Key_Control, Qt.Key.Key_Alt):
|
|
407
|
+
# Check if both Ctrl and Alt are released
|
|
408
|
+
modifiers = event.modifiers()
|
|
409
|
+
if not (modifiers & Qt.KeyboardModifier.ControlModifier) and not (modifiers & Qt.KeyboardModifier.AltModifier):
|
|
410
|
+
if self._original_drag_mode is not None:
|
|
411
|
+
# Restore original drag mode
|
|
412
|
+
self._graphics_view.setDragMode(self._original_drag_mode)
|
|
413
|
+
print(f"[DEBUG] Ctrl/Alt released: restored graphics view drag mode to: {self._original_drag_mode}")
|
|
414
|
+
self._original_drag_mode = None
|
|
415
|
+
super().keyReleaseEvent(event)
|
|
416
|
+
|
|
417
|
+
def _show_error(self, message: str):
|
|
418
|
+
"""Show error message."""
|
|
419
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
420
|
+
QMessageBox.critical(self, lang('error'), message)
|
|
421
|
+
|
|
422
|
+
def _reset_to_default_state(self):
|
|
423
|
+
"""Reset to default state with language-appropriate text."""
|
|
424
|
+
self._status_label.setText(lang('no_map_preview'))
|
|
425
|
+
self._status_label.show()
|
|
426
|
+
self._graphics_view.hide()
|
|
427
|
+
self._open_button.setText(lang('open_map'))
|
|
428
|
+
|
|
429
|
+
def eventFilter(self, obj, event):
|
|
430
|
+
"""Handle mouse events for the graphics view."""
|
|
431
|
+
if obj == self._graphics_view.viewport() and self._map_item is not None:
|
|
432
|
+
if event.type() == event.Type.MouseMove:
|
|
433
|
+
self._handle_mouse_move(event.pos())
|
|
434
|
+
# Handle drag initiation during mouse move
|
|
435
|
+
if self._is_dragging and self._drag_start_pos is not None:
|
|
436
|
+
# Calculate drag distance to determine if it's a valid drag
|
|
437
|
+
drag_distance = abs(event.pos().x() - self._drag_start_pos.x()) + abs(event.pos().y() - self._drag_start_pos.y())
|
|
438
|
+
print(f"[DEBUG] Mouse move - drag distance: {drag_distance}, is_dragging: {self._is_dragging}")
|
|
439
|
+
if drag_distance > 10: # Minimum drag distance
|
|
440
|
+
print(f"[DEBUG] Starting drag operation")
|
|
441
|
+
# Use QApplication to start the drag from the viewport
|
|
442
|
+
from PyQt6.QtWidgets import QApplication
|
|
443
|
+
# Force the viewport to have focus for drag operations
|
|
444
|
+
self._graphics_view.viewport().setFocus()
|
|
445
|
+
self._initiate_drag(self._drag_cell_info)
|
|
446
|
+
# Reset drag state after initiating drag
|
|
447
|
+
self._drag_start_pos = None
|
|
448
|
+
self._drag_cell_info = None
|
|
449
|
+
self._is_dragging = False
|
|
450
|
+
elif event.type() == event.Type.MouseButtonPress:
|
|
451
|
+
# Ensure cursor remains cross during and after click
|
|
452
|
+
self._graphics_view.viewport().setCursor(Qt.CursorShape.CrossCursor)
|
|
453
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
454
|
+
# Check if Ctrl or Alt is pressed for drag functionality
|
|
455
|
+
modifiers = event.modifiers()
|
|
456
|
+
print(f"[DEBUG] Mouse press - modifiers: {modifiers}")
|
|
457
|
+
if modifiers & (Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier):
|
|
458
|
+
# Store drag info for potential drag
|
|
459
|
+
cell_info = self._get_cell_at_pos(event.pos())
|
|
460
|
+
if cell_info is not None:
|
|
461
|
+
self._drag_start_pos = event.pos()
|
|
462
|
+
self._drag_cell_info = cell_info
|
|
463
|
+
self._is_dragging = True
|
|
464
|
+
print(f"[DEBUG] Drag preparation: cell ({cell_info.x}, {cell_info.y})")
|
|
465
|
+
else:
|
|
466
|
+
self._handle_mouse_click(event.pos())
|
|
467
|
+
elif event.button() == Qt.MouseButton.RightButton:
|
|
468
|
+
self._handle_right_click(event.pos())
|
|
469
|
+
elif event.type() == event.Type.MouseButtonRelease:
|
|
470
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
471
|
+
# Reset drag state on mouse release
|
|
472
|
+
self._drag_start_pos = None
|
|
473
|
+
self._drag_cell_info = None
|
|
474
|
+
self._is_dragging = False
|
|
475
|
+
elif event.type() == event.Type.Wheel:
|
|
476
|
+
self._handle_wheel_event(event)
|
|
477
|
+
return True # Consume the event
|
|
478
|
+
elif event.type() == event.Type.Enter:
|
|
479
|
+
self._graphics_view.viewport().setCursor(Qt.CursorShape.CrossCursor)
|
|
480
|
+
elif event.type() == event.Type.Leave:
|
|
481
|
+
self._graphics_view.viewport().unsetCursor()
|
|
482
|
+
|
|
483
|
+
return super().eventFilter(obj, event)
|
|
484
|
+
|
|
485
|
+
def _handle_mouse_move(self, pos):
|
|
486
|
+
"""Handle mouse move events."""
|
|
487
|
+
cell_info = self._get_cell_at_pos(pos)
|
|
488
|
+
|
|
489
|
+
if cell_info != self._current_cell:
|
|
490
|
+
self._current_cell = cell_info
|
|
491
|
+
self._update_highlight()
|
|
492
|
+
self.currentChanged.emit(cell_info)
|
|
493
|
+
|
|
494
|
+
def _handle_mouse_click(self, pos):
|
|
495
|
+
"""Handle mouse click events."""
|
|
496
|
+
cell_info = self._get_cell_at_pos(pos)
|
|
497
|
+
|
|
498
|
+
if cell_info != self._selected_cell:
|
|
499
|
+
self._selected_cell = cell_info
|
|
500
|
+
self._update_selection()
|
|
501
|
+
self.selectChanged.emit(cell_info)
|
|
502
|
+
|
|
503
|
+
def _handle_right_click(self, pos):
|
|
504
|
+
"""Handle right-click events."""
|
|
505
|
+
cell_info = self._get_cell_at_pos(pos)
|
|
506
|
+
self.rightClicked.emit(cell_info)
|
|
507
|
+
|
|
508
|
+
def _initiate_drag(self, cell_info: CellInfo):
|
|
509
|
+
"""Initiate drag operation with cell data."""
|
|
510
|
+
print(f"[DEBUG] Initiating drag for cell ({cell_info.x}, {cell_info.y})")
|
|
511
|
+
|
|
512
|
+
# Generate drag data based on cell contents
|
|
513
|
+
if cell_info.objects:
|
|
514
|
+
# If cell has objects, create array of all objects with screenshots
|
|
515
|
+
drag_data = []
|
|
516
|
+
|
|
517
|
+
# Extract cell screenshot if map image is available
|
|
518
|
+
cell_image = None
|
|
519
|
+
if self.image is not None and not self.image.isNull():
|
|
520
|
+
try:
|
|
521
|
+
# Calculate cell size based on map image dimensions
|
|
522
|
+
cell_width = self.image.width() // self._map_width
|
|
523
|
+
cell_height = self.image.height() // self._map_height
|
|
524
|
+
|
|
525
|
+
# Calculate the position of this cell in the image
|
|
526
|
+
cell_x_offset = cell_info.x * cell_width
|
|
527
|
+
cell_y_offset = cell_info.y * cell_height
|
|
528
|
+
|
|
529
|
+
# Extract the cell region
|
|
530
|
+
if (cell_x_offset + cell_width <= self.image.width() and
|
|
531
|
+
cell_y_offset + cell_height <= self.image.height() and
|
|
532
|
+
cell_x_offset >= 0 and cell_y_offset >= 0):
|
|
533
|
+
|
|
534
|
+
cell_image = self.image.copy(cell_x_offset, cell_y_offset, cell_width, cell_height)
|
|
535
|
+
print(f"[DEBUG] Extracted cell screenshot for ({cell_info.x}, {cell_info.y}): {cell_image.size()}")
|
|
536
|
+
except Exception as e:
|
|
537
|
+
print(f"[DEBUG] Failed to extract cell screenshot: {e}")
|
|
538
|
+
|
|
539
|
+
# Create object data for each object with the same screenshot
|
|
540
|
+
for obj in cell_info.objects:
|
|
541
|
+
obj_data = {
|
|
542
|
+
"x": cell_info.x,
|
|
543
|
+
"y": cell_info.y,
|
|
544
|
+
"type": obj.get('type', 'Unknown'),
|
|
545
|
+
"method": obj.get('method', ''),
|
|
546
|
+
"id": obj.get('id', 'unknown'),
|
|
547
|
+
"name": obj.get('name', obj.get('id', 'unknown'))
|
|
548
|
+
}
|
|
549
|
+
drag_data.append(obj_data)
|
|
550
|
+
else:
|
|
551
|
+
# If cell has no objects, create point data (no screenshot)
|
|
552
|
+
drag_data = {
|
|
553
|
+
"x": cell_info.x,
|
|
554
|
+
"y": cell_info.y,
|
|
555
|
+
"type": "Point",
|
|
556
|
+
"method": "Point",
|
|
557
|
+
"id": f"{cell_info.x}◇{cell_info.y}",
|
|
558
|
+
"name": f"p{cell_info.x}◇{cell_info.y}"
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
# Convert to JSON string
|
|
562
|
+
json_data = json.dumps(drag_data, ensure_ascii=False, indent=2)
|
|
563
|
+
print(f"[DEBUG] Drag data: {json_data}")
|
|
564
|
+
|
|
565
|
+
# Create mime data
|
|
566
|
+
mime_data = QMimeData()
|
|
567
|
+
mime_data.setText(json_data)
|
|
568
|
+
mime_data.setData("application/json", json_data.encode('utf-8'))
|
|
569
|
+
|
|
570
|
+
# Add cell screenshot if available
|
|
571
|
+
if cell_info.objects and cell_image is not None and not cell_image.isNull():
|
|
572
|
+
mime_data.setImageData(cell_image)
|
|
573
|
+
print(f"[DEBUG] Added cell screenshot to drag data")
|
|
574
|
+
|
|
575
|
+
# Create drag object
|
|
576
|
+
drag = QDrag(self._graphics_view.viewport())
|
|
577
|
+
drag.setMimeData(mime_data)
|
|
578
|
+
|
|
579
|
+
# Start drag operation
|
|
580
|
+
result = drag.exec(Qt.DropAction.CopyAction | Qt.DropAction.MoveAction)
|
|
581
|
+
print(f"[DEBUG] Drag result: {result}")
|
|
582
|
+
|
|
583
|
+
return result
|
|
584
|
+
|
|
585
|
+
def _handle_wheel_event(self, event):
|
|
586
|
+
"""Handle mouse wheel events for zooming."""
|
|
587
|
+
if self._map_item is None:
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
# Get the wheel delta
|
|
591
|
+
delta = event.angleDelta().y()
|
|
592
|
+
|
|
593
|
+
# Calculate zoom factor
|
|
594
|
+
zoom_factor = 1.1 if delta > 0 else 0.9
|
|
595
|
+
|
|
596
|
+
# Limit zoom range
|
|
597
|
+
current_scale = self._graphics_view.transform().m11()
|
|
598
|
+
if (current_scale > 5.0 and zoom_factor > 1) or (current_scale < 0.1 and zoom_factor < 1):
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
# Get the position before scaling
|
|
602
|
+
old_pos = self._graphics_view.mapToScene(event.position().toPoint())
|
|
603
|
+
|
|
604
|
+
# Apply scaling
|
|
605
|
+
self._graphics_view.scale(zoom_factor, zoom_factor)
|
|
606
|
+
|
|
607
|
+
# Get the position after scaling
|
|
608
|
+
new_pos = self._graphics_view.mapToScene(event.position().toPoint())
|
|
609
|
+
|
|
610
|
+
# Adjust the scroll position to keep the mouse position fixed
|
|
611
|
+
delta_pos = new_pos - old_pos
|
|
612
|
+
self._graphics_view.translate(delta_pos.x(), delta_pos.y())
|
|
613
|
+
|
|
614
|
+
print(f"[DEBUG] Zoom: {current_scale:.2f} -> {self._graphics_view.transform().m11():.2f}") # 调试输出
|
|
615
|
+
|
|
616
|
+
def _get_cell_at_pos(self, pos) -> Optional[CellInfo]:
|
|
617
|
+
"""Get cell information at the given viewport position."""
|
|
618
|
+
if self._map_item is None:
|
|
619
|
+
return None
|
|
620
|
+
|
|
621
|
+
# Map viewport position to scene coordinates
|
|
622
|
+
scene_pos = self._graphics_view.mapToScene(pos)
|
|
623
|
+
|
|
624
|
+
# Convert scene coordinates to cell coordinates
|
|
625
|
+
x = int(scene_pos.x() / self._cell_size)
|
|
626
|
+
y = int(scene_pos.y() / self._cell_size)
|
|
627
|
+
|
|
628
|
+
# Check bounds
|
|
629
|
+
if 0 <= x < self._map_width and 0 <= y < self._map_height:
|
|
630
|
+
return self._cell_info_grid[y][x]
|
|
631
|
+
|
|
632
|
+
return None
|
|
633
|
+
|
|
634
|
+
def _update_highlight(self):
|
|
635
|
+
"""Update hover highlight."""
|
|
636
|
+
# Remove existing highlight
|
|
637
|
+
if self._highlight_item is not None:
|
|
638
|
+
self._scene.removeItem(self._highlight_item)
|
|
639
|
+
self._highlight_item = None
|
|
640
|
+
|
|
641
|
+
# Add new highlight if there's a current cell
|
|
642
|
+
if self._current_cell is not None:
|
|
643
|
+
rect = self._get_cell_rect(self._current_cell.x, self._current_cell.y)
|
|
644
|
+
self._highlight_item = self._scene.addRect(rect, QPen(Qt.PenStyle.NoPen), QBrush(self._hover_color))
|
|
645
|
+
self._highlight_item.setZValue(10) # Above map, below selection
|
|
646
|
+
|
|
647
|
+
def _update_selection(self):
|
|
648
|
+
"""Update cell selection."""
|
|
649
|
+
# Remove existing selection
|
|
650
|
+
if self._selection_item is not None:
|
|
651
|
+
self._scene.removeItem(self._selection_item)
|
|
652
|
+
self._selection_item = None
|
|
653
|
+
|
|
654
|
+
# Add new selection if there's a selected cell
|
|
655
|
+
if self._selected_cell is not None:
|
|
656
|
+
rect = self._get_cell_rect(self._selected_cell.x, self._selected_cell.y)
|
|
657
|
+
|
|
658
|
+
# Calculate contrasting color for the border
|
|
659
|
+
border_color = self._get_contrasting_color()
|
|
660
|
+
pen = QPen(border_color, self._selection_width)
|
|
661
|
+
pen.setStyle(Qt.PenStyle.SolidLine)
|
|
662
|
+
|
|
663
|
+
self._selection_item = self._scene.addRect(rect, pen)
|
|
664
|
+
self._selection_item.setZValue(20) # Above everything
|
|
665
|
+
|
|
666
|
+
def _get_cell_rect(self, x: int, y: int) -> QRectF:
|
|
667
|
+
"""Get rectangle for a cell."""
|
|
668
|
+
left = x * self._cell_size
|
|
669
|
+
top = y * self._cell_size
|
|
670
|
+
return QRectF(left, top, self._cell_size, self._cell_size)
|
|
671
|
+
|
|
672
|
+
def _get_contrasting_color(self) -> QColor:
|
|
673
|
+
"""Get a contrasting color for selection border."""
|
|
674
|
+
# Use a bright color that contrasts with most backgrounds
|
|
675
|
+
return QColor(255, 0, 255) # Magenta
|
|
676
|
+
|
|
677
|
+
# Public properties
|
|
678
|
+
@property
|
|
679
|
+
def current(self) -> Optional[CellInfo]:
|
|
680
|
+
"""Get current cell under mouse cursor."""
|
|
681
|
+
return self._current_cell
|
|
682
|
+
|
|
683
|
+
@property
|
|
684
|
+
def selected(self) -> Optional[CellInfo]:
|
|
685
|
+
"""Get selected cell."""
|
|
686
|
+
return self._selected_cell
|
|
687
|
+
|
|
688
|
+
@property
|
|
689
|
+
def map_width(self) -> int:
|
|
690
|
+
"""Get map width."""
|
|
691
|
+
return self._map_width
|
|
692
|
+
|
|
693
|
+
@property
|
|
694
|
+
def map_height(self) -> int:
|
|
695
|
+
"""Get map height."""
|
|
696
|
+
return self._map_height
|
|
697
|
+
|
|
698
|
+
@property
|
|
699
|
+
def image(self) -> Optional[QPixmap]:
|
|
700
|
+
"""Get the current map image."""
|
|
701
|
+
if self._map_item is not None:
|
|
702
|
+
return self._map_item.pixmap()
|
|
703
|
+
return None
|
|
704
|
+
|
|
705
|
+
def clear_selection(self):
|
|
706
|
+
"""Clear current selection."""
|
|
707
|
+
self._selected_cell = None
|
|
708
|
+
self._update_selection()
|
|
709
|
+
self.selectChanged.emit(None)
|
|
710
|
+
|
|
711
|
+
def clear_current(self):
|
|
712
|
+
"""Clear current hover cell."""
|
|
713
|
+
self._current_cell = None
|
|
714
|
+
self._update_highlight()
|
|
715
|
+
self.currentChanged.emit(None)
|
|
716
|
+
|
|
717
|
+
def cleanup(self):
|
|
718
|
+
"""Clean up temporary files."""
|
|
719
|
+
if self._temp_image_path and os.path.exists(self._temp_image_path):
|
|
720
|
+
try:
|
|
721
|
+
os.remove(self._temp_image_path)
|
|
722
|
+
print(f"[DEBUG] Cleaned up temporary file: {self._temp_image_path}") # 调试输出
|
|
723
|
+
except Exception as e:
|
|
724
|
+
print(f"[DEBUG] Failed to cleanup temporary file: {e}") # 调试输出
|
|
725
|
+
|
|
726
|
+
def __del__(self):
|
|
727
|
+
"""Destructor to ensure cleanup."""
|
|
728
|
+
self.cleanup()
|