Sat, 04 Jun 2016 09:22:51 +0200
Code cleanup
15 | 1 | # This file is part of the Printrun suite. |
2 | # | |
3 | # Printrun is free software: you can redistribute it and/or modify | |
4 | # it under the terms of the GNU General Public License as published by | |
5 | # the Free Software Foundation, either version 3 of the License, or | |
6 | # (at your option) any later version. | |
7 | # | |
8 | # Printrun is distributed in the hope that it will be useful, | |
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | # GNU General Public License for more details. | |
12 | # | |
13 | # You should have received a copy of the GNU General Public License | |
14 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
15 | ||
16 | import wx | |
17 | import math | |
18 | from .bufferedcanvas import BufferedCanvas | |
19 | from printrun.utils import imagefile | |
20 | ||
21 | def sign(n): | |
22 | if n < 0: return -1 | |
23 | elif n > 0: return 1 | |
24 | else: return 0 | |
25 | ||
26 | class XYButtons(BufferedCanvas): | |
27 | keypad_positions = { | |
28 | 0: (106, 100), | |
29 | 1: (86, 83), | |
30 | 2: (68, 65), | |
31 | 3: (53, 50) | |
32 | } | |
33 | corner_size = (49, 49) | |
34 | corner_inset = (7, 13) | |
35 | label_overlay_positions = { | |
36 | 1: (145, 98.5, 9), | |
37 | 2: (160.5, 83.5, 10.6), | |
38 | 3: (178, 66, 13), | |
39 | 4: (197.3, 46.3, 13.3) | |
40 | } | |
41 | concentric_circle_radii = [0, 17, 45, 69, 94, 115] | |
42 | concentric_inset = 11 | |
43 | center = (124, 121) | |
44 | spacer = 7 | |
45 | imagename = "control_xy.png" | |
46 | corner_to_axis = { | |
47 | -1: "center", | |
48 | 0: "x", | |
49 | 1: "z", | |
50 | 2: "y", | |
51 | 3: "all", | |
52 | } | |
53 | ||
54 | def __init__(self, parent, moveCallback = None, cornerCallback = None, spacebarCallback = None, bgcolor = "#FFFFFF", ID=-1, zcallback=None): | |
55 | self.bg_bmp = wx.Image(imagefile(self.imagename), wx.BITMAP_TYPE_PNG).ConvertToBitmap() | |
56 | self.keypad_bmp = wx.Image(imagefile("arrow_keys.png"), wx.BITMAP_TYPE_PNG).ConvertToBitmap() | |
57 | self.keypad_idx = -1 | |
58 | self.quadrant = None | |
59 | self.concentric = None | |
60 | self.corner = None | |
61 | self.moveCallback = moveCallback | |
62 | self.cornerCallback = cornerCallback | |
63 | self.spacebarCallback = spacebarCallback | |
64 | self.zCallback = zcallback | |
65 | self.enabled = False | |
66 | # Remember the last clicked buttons, so we can repeat when spacebar pressed | |
67 | self.lastMove = None | |
68 | self.lastCorner = None | |
69 | ||
70 | self.bgcolor = wx.Colour() | |
71 | self.bgcolor.SetFromName(bgcolor) | |
72 | self.bgcolormask = wx.Colour(self.bgcolor.Red(), self.bgcolor.Green(), self.bgcolor.Blue(), 128) | |
73 | ||
74 | BufferedCanvas.__init__(self, parent, ID, size=self.bg_bmp.GetSize()) | |
75 | ||
76 | self.bind_events() | |
77 | ||
78 | def bind_events(self): | |
79 | # Set up mouse and keyboard event capture | |
80 | self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) | |
81 | self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown) | |
82 | self.Bind(wx.EVT_MOTION, self.OnMotion) | |
83 | self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow) | |
84 | self.Bind(wx.EVT_KEY_UP, self.OnKey) | |
85 | wx.GetTopLevelParent(self).Bind(wx.EVT_CHAR_HOOK, self.OnTopLevelKey) | |
86 | ||
87 | def disable(self): | |
88 | self.enabled = False | |
89 | self.update() | |
90 | ||
91 | def enable(self): | |
92 | self.enabled = True | |
93 | self.update() | |
94 | ||
95 | def repeatLast(self): | |
96 | if self.lastMove: | |
97 | self.moveCallback(*self.lastMove) | |
98 | if self.lastCorner: | |
99 | self.cornerCallback(self.corner_to_axis[self.lastCorner]) | |
100 | ||
101 | def clearRepeat(self): | |
102 | self.lastMove = None | |
103 | self.lastCorner = None | |
104 | ||
105 | def distanceToLine(self, pos, x1, y1, x2, y2): | |
106 | xlen = x2 - x1 | |
107 | ylen = y2 - y1 | |
108 | pxlen = x1 - pos.x | |
109 | pylen = y1 - pos.y | |
110 | return abs(xlen * pylen - ylen * pxlen) / math.sqrt(xlen ** 2 + ylen ** 2) | |
111 | ||
112 | def distanceToPoint(self, x1, y1, x2, y2): | |
113 | return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) | |
114 | ||
115 | def cycleKeypadIndex(self): | |
116 | idx = self.keypad_idx + 1 | |
117 | if idx > 2: idx = 0 | |
118 | return idx | |
119 | ||
120 | def setKeypadIndex(self, idx): | |
121 | self.keypad_idx = idx | |
122 | self.update() | |
123 | ||
124 | def getMovement(self): | |
125 | xdir = [1, 0, -1, 0, 0, 0][self.quadrant] | |
126 | ydir = [0, 1, 0, -1, 0, 0][self.quadrant] | |
127 | zdir = [0, 0, 0, 0, 1, -1][self.quadrant] | |
128 | magnitude = math.pow(10, self.concentric - 2) | |
129 | if not zdir == 0: | |
130 | magnitude = min(magnitude, 10) | |
131 | return (magnitude * xdir, magnitude * ydir, magnitude * zdir) | |
132 | ||
133 | def lookupConcentric(self, radius): | |
134 | idx = 0 | |
135 | for r in self.concentric_circle_radii[1:]: | |
136 | if radius < r: | |
137 | return idx | |
138 | idx += 1 | |
139 | return len(self.concentric_circle_radii) | |
140 | ||
141 | def getQuadrantConcentricFromPosition(self, pos): | |
142 | rel_x = pos[0] - self.center[0] | |
143 | rel_y = pos[1] - self.center[1] | |
144 | radius = math.sqrt(rel_x ** 2 + rel_y ** 2) | |
145 | if rel_x > rel_y and rel_x > -rel_y: | |
146 | quadrant = 0 # Right | |
147 | elif rel_x <= rel_y and rel_x > -rel_y: | |
148 | quadrant = 3 # Down | |
149 | elif rel_x > rel_y and rel_x < -rel_y: | |
150 | quadrant = 1 # Up | |
151 | else: | |
152 | quadrant = 2 # Left | |
153 | ||
154 | idx = self.lookupConcentric(radius) | |
155 | return (quadrant, idx) | |
156 | ||
157 | def mouseOverKeypad(self, mpos): | |
158 | for idx, kpos in self.keypad_positions.items(): | |
159 | radius = self.distanceToPoint(mpos[0], mpos[1], kpos[0], kpos[1]) | |
160 | if radius < 9: | |
161 | return idx | |
162 | return None | |
163 | ||
164 | def drawPartialPie(self, gc, center, r1, r2, angle1, angle2): | |
165 | p1 = wx.Point(center.x + r1 * math.cos(angle1), center.y + r1 * math.sin(angle1)) | |
166 | ||
167 | path = gc.CreatePath() | |
168 | path.MoveToPoint(p1.x, p1.y) | |
169 | path.AddArc(center.x, center.y, r1, angle1, angle2, True) | |
170 | path.AddArc(center.x, center.y, r2, angle2, angle1, False) | |
171 | path.AddLineToPoint(p1.x, p1.y) | |
172 | gc.DrawPath(path) | |
173 | ||
174 | def highlightQuadrant(self, gc, quadrant, concentric): | |
175 | assert(quadrant >= 0 and quadrant <= 3) | |
176 | assert(concentric >= 0 and concentric <= 4) | |
177 | ||
178 | inner_ring_radius = self.concentric_inset | |
179 | # fudge = math.pi*0.002 | |
180 | fudge = -0.02 | |
181 | center = wx.Point(self.center[0], self.center[1]) | |
182 | if quadrant == 0: | |
183 | a1, a2 = (-math.pi * 0.25, math.pi * 0.25) | |
184 | center.x += inner_ring_radius | |
185 | elif quadrant == 1: | |
186 | a1, a2 = (math.pi * 1.25, math.pi * 1.75) | |
187 | center.y -= inner_ring_radius | |
188 | elif quadrant == 2: | |
189 | a1, a2 = (math.pi * 0.75, math.pi * 1.25) | |
190 | center.x -= inner_ring_radius | |
191 | elif quadrant == 3: | |
192 | a1, a2 = (math.pi * 0.25, math.pi * 0.75) | |
193 | center.y += inner_ring_radius | |
194 | ||
195 | r1 = self.concentric_circle_radii[concentric] | |
196 | r2 = self.concentric_circle_radii[concentric + 1] | |
197 | ||
198 | self.drawPartialPie(gc, center, r1 - inner_ring_radius, r2 - inner_ring_radius, a1 + fudge, a2 - fudge) | |
199 | ||
200 | def drawCorner(self, gc, x, y, angle = 0.0): | |
201 | w, h = self.corner_size | |
202 | ||
203 | gc.PushState() | |
204 | gc.Translate(x, y) | |
205 | gc.Rotate(angle) | |
206 | path = gc.CreatePath() | |
207 | path.MoveToPoint(-w / 2, -h / 2) | |
208 | path.AddLineToPoint(w / 2, -h / 2) | |
209 | path.AddLineToPoint(w / 2, -h / 2 + h / 4) | |
210 | path.AddLineToPoint(w / 12, h / 12) | |
211 | path.AddLineToPoint(-w / 2 + w / 4, h / 2) | |
212 | path.AddLineToPoint(-w / 2, h / 2) | |
213 | path.AddLineToPoint(-w / 2, -h / 2) | |
214 | gc.DrawPath(path) | |
215 | gc.PopState() | |
216 | ||
217 | def highlightCorner(self, gc, corner = 0): | |
218 | w, h = self.corner_size | |
219 | xinset, yinset = self.corner_inset | |
220 | cx, cy = self.center | |
221 | ww, wh = self.GetSizeTuple() | |
222 | ||
223 | if corner == 0: | |
224 | x, y = (cx - ww / 2 + xinset + 1, cy - wh / 2 + yinset) | |
225 | self.drawCorner(gc, x + w / 2, y + h / 2, 0) | |
226 | elif corner == 1: | |
227 | x, y = (cx + ww / 2 - xinset, cy - wh / 2 + yinset) | |
228 | self.drawCorner(gc, x - w / 2, y + h / 2, math.pi / 2) | |
229 | elif corner == 2: | |
230 | x, y = (cx + ww / 2 - xinset, cy + wh / 2 - yinset - 1) | |
231 | self.drawCorner(gc, x - w / 2, y - h / 2, math.pi) | |
232 | elif corner == 3: | |
233 | x, y = (cx - ww / 2 + xinset + 1, cy + wh / 2 - yinset - 1) | |
234 | self.drawCorner(gc, x + w / 2, y - h / 2, math.pi * 3 / 2) | |
235 | ||
236 | def drawCenteredDisc(self, gc, radius): | |
237 | cx, cy = self.center | |
238 | gc.DrawEllipse(cx - radius, cy - radius, radius * 2, radius * 2) | |
239 | ||
240 | def draw(self, dc, w, h): | |
241 | dc.SetBackground(wx.Brush(self.bgcolor)) | |
242 | dc.Clear() | |
243 | gc = wx.GraphicsContext.Create(dc) | |
244 | ||
245 | if self.bg_bmp: | |
246 | w, h = (self.bg_bmp.GetWidth(), self.bg_bmp.GetHeight()) | |
247 | gc.DrawBitmap(self.bg_bmp, 0, 0, w, h) | |
248 | ||
249 | if self.enabled and self.IsEnabled(): | |
250 | # Brush and pen for grey overlay when mouse hovers over | |
251 | gc.SetPen(wx.Pen(wx.Colour(100, 100, 100, 172), 4)) | |
252 | gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 128))) | |
253 | ||
254 | if self.concentric is not None: | |
255 | if self.concentric < len(self.concentric_circle_radii): | |
256 | if self.concentric == 0: | |
257 | self.drawCenteredDisc(gc, self.concentric_circle_radii[1]) | |
258 | elif self.quadrant is not None: | |
259 | self.highlightQuadrant(gc, self.quadrant, self.concentric) | |
260 | elif self.corner is not None: | |
261 | self.highlightCorner(gc, self.corner) | |
262 | ||
263 | if self.keypad_idx >= 0: | |
264 | padw, padh = (self.keypad_bmp.GetWidth(), self.keypad_bmp.GetHeight()) | |
265 | pos = self.keypad_positions[self.keypad_idx] | |
266 | pos = (pos[0] - padw / 2 - 3, pos[1] - padh / 2 - 3) | |
267 | gc.DrawBitmap(self.keypad_bmp, pos[0], pos[1], padw, padh) | |
268 | ||
269 | # Draw label overlays | |
270 | gc.SetPen(wx.Pen(wx.Colour(255, 255, 255, 128), 1)) | |
271 | gc.SetBrush(wx.Brush(wx.Colour(255, 255, 255, 128 + 64))) | |
272 | for idx, kpos in self.label_overlay_positions.items(): | |
273 | if idx != self.concentric: | |
274 | r = kpos[2] | |
275 | gc.DrawEllipse(kpos[0] - r, kpos[1] - r, r * 2, r * 2) | |
276 | else: | |
277 | gc.SetPen(wx.Pen(self.bgcolor, 0)) | |
278 | gc.SetBrush(wx.Brush(self.bgcolormask)) | |
279 | gc.DrawRectangle(0, 0, w, h) | |
280 | # Used to check exact position of keypad dots, should we ever resize the bg image | |
281 | # for idx, kpos in self.label_overlay_positions.items(): | |
282 | # dc.DrawCircle(kpos[0], kpos[1], kpos[2]) | |
283 | ||
284 | # ------ # | |
285 | # Events # | |
286 | # ------ # | |
287 | def OnTopLevelKey(self, evt): | |
288 | # Let user press escape on any control, and return focus here | |
289 | if evt.GetKeyCode() == wx.WXK_ESCAPE: | |
290 | self.SetFocus() | |
291 | evt.Skip() | |
292 | ||
293 | def OnKey(self, evt): | |
294 | if not self.enabled: | |
295 | return | |
296 | if self.keypad_idx >= 0: | |
297 | if evt.GetKeyCode() == wx.WXK_TAB: | |
298 | self.setKeypadIndex(self.cycleKeypadIndex()) | |
299 | elif evt.GetKeyCode() == wx.WXK_UP: | |
300 | self.quadrant = 1 | |
301 | elif evt.GetKeyCode() == wx.WXK_DOWN: | |
302 | self.quadrant = 3 | |
303 | elif evt.GetKeyCode() == wx.WXK_LEFT: | |
304 | self.quadrant = 2 | |
305 | elif evt.GetKeyCode() == wx.WXK_RIGHT: | |
306 | self.quadrant = 0 | |
307 | elif evt.GetKeyCode() == wx.WXK_PAGEUP: | |
308 | self.quadrant = 4 | |
309 | elif evt.GetKeyCode() == wx.WXK_PAGEDOWN: | |
310 | self.quadrant = 5 | |
311 | else: | |
312 | evt.Skip() | |
313 | return | |
314 | ||
315 | self.concentric = self.keypad_idx | |
316 | x, y, z = self.getMovement() | |
317 | ||
318 | if x != 0 or y != 0 and self.moveCallback: | |
319 | self.moveCallback(x, y) | |
320 | if z != 0 and self.zCallback: | |
321 | self.zCallback(z) | |
322 | elif evt.GetKeyCode() == wx.WXK_SPACE: | |
323 | self.spacebarCallback() | |
324 | ||
325 | def OnMotion(self, event): | |
326 | if not self.enabled: | |
327 | return | |
328 | ||
329 | oldcorner = self.corner | |
330 | oldq, oldc = self.quadrant, self.concentric | |
331 | ||
332 | mpos = event.GetPosition() | |
333 | idx = self.mouseOverKeypad(mpos) | |
334 | self.quadrant = None | |
335 | self.concentric = None | |
336 | if idx is None: | |
337 | center = wx.Point(self.center[0], self.center[1]) | |
338 | riseDist = self.distanceToLine(mpos, center.x - 1, center.y - 1, center.x + 1, center.y + 1) | |
339 | fallDist = self.distanceToLine(mpos, center.x - 1, center.y + 1, center.x + 1, center.y - 1) | |
340 | self.quadrant, self.concentric = self.getQuadrantConcentricFromPosition(mpos) | |
341 | ||
342 | # If mouse hovers in space between quadrants, don't commit to a quadrant | |
343 | if riseDist <= self.spacer or fallDist <= self.spacer: | |
344 | self.quadrant = None | |
345 | ||
346 | cx, cy = self.center | |
347 | if mpos.x < cx and mpos.y < cy: | |
348 | self.corner = 0 | |
349 | if mpos.x >= cx and mpos.y < cy: | |
350 | self.corner = 1 | |
351 | if mpos.x >= cx and mpos.y >= cy: | |
352 | self.corner = 2 | |
353 | if mpos.x < cx and mpos.y >= cy: | |
354 | self.corner = 3 | |
355 | ||
356 | if oldq != self.quadrant or oldc != self.concentric or oldcorner != self.corner: | |
357 | self.update() | |
358 | ||
359 | def OnLeftDown(self, event): | |
360 | if not self.enabled: | |
361 | return | |
362 | ||
363 | # Take focus when clicked so that arrow keys can control movement | |
364 | self.SetFocus() | |
365 | ||
366 | mpos = event.GetPosition() | |
367 | ||
368 | idx = self.mouseOverKeypad(mpos) | |
369 | if idx is None: | |
370 | self.quadrant, self.concentric = self.getQuadrantConcentricFromPosition(mpos) | |
371 | if self.concentric is not None: | |
372 | if self.concentric < len(self.concentric_circle_radii): | |
373 | if self.concentric == 0: | |
374 | self.lastCorner = -1 | |
375 | self.lastMove = None | |
376 | self.cornerCallback(self.corner_to_axis[-1]) | |
377 | elif self.quadrant is not None: | |
378 | x, y, z = self.getMovement() | |
379 | if self.moveCallback: | |
380 | self.lastMove = (x, y) | |
381 | self.lastCorner = None | |
382 | self.moveCallback(x, y) | |
383 | elif self.corner is not None: | |
384 | if self.cornerCallback: | |
385 | self.lastCorner = self.corner | |
386 | self.lastMove = None | |
387 | self.cornerCallback(self.corner_to_axis[self.corner]) | |
388 | else: | |
389 | if self.keypad_idx == idx: | |
390 | self.setKeypadIndex(-1) | |
391 | else: | |
392 | self.setKeypadIndex(idx) | |
393 | ||
394 | def OnLeaveWindow(self, evt): | |
395 | self.quadrant = None | |
396 | self.concentric = None | |
397 | self.update() | |
398 | ||
399 | class XYButtonsMini(XYButtons): | |
400 | imagename = "control_mini.png" | |
401 | center = (57, 56.5) | |
402 | concentric_circle_radii = [0, 30.3] | |
403 | corner_inset = (5, 5) | |
404 | corner_size = (50, 50) | |
405 | outer_radius = 31 | |
406 | corner_to_axis = { | |
407 | 0: "x", | |
408 | 1: "z", | |
409 | 2: "y", | |
410 | 3: "center", | |
411 | } | |
412 | ||
413 | def bind_events(self): | |
414 | # Set up mouse and keyboard event capture | |
415 | self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) | |
416 | self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown) | |
417 | self.Bind(wx.EVT_MOTION, self.OnMotion) | |
418 | self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow) | |
419 | ||
420 | def OnMotion(self, event): | |
421 | if not self.enabled: | |
422 | return | |
423 | ||
424 | oldcorner = self.corner | |
425 | oldq, oldc = self.quadrant, self.concentric | |
426 | ||
427 | mpos = event.GetPosition() | |
428 | ||
429 | self.quadrant, self.concentric = self.getQuadrantConcentricFromPosition(mpos) | |
430 | ||
431 | cx, cy = XYButtonsMini.center | |
432 | if mpos.x < cx and mpos.y < cy: | |
433 | self.corner = 0 | |
434 | if mpos.x >= cx and mpos.y < cy: | |
435 | self.corner = 1 | |
436 | if mpos.x >= cx and mpos.y >= cy: | |
437 | self.corner = 2 | |
438 | if mpos.x < cx and mpos.y >= cy: | |
439 | self.corner = 3 | |
440 | ||
441 | if oldq != self.quadrant or oldc != self.concentric or oldcorner != self.corner: | |
442 | self.update() | |
443 | ||
444 | def OnLeftDown(self, event): | |
445 | if not self.enabled: | |
446 | return | |
447 | ||
448 | # Take focus when clicked so that arrow keys can control movement | |
449 | self.SetFocus() | |
450 | ||
451 | mpos = event.GetPosition() | |
452 | ||
453 | self.quadrant, self.concentric = self.getQuadrantConcentricFromPosition(mpos) | |
454 | if self.concentric is not None: | |
455 | if self.concentric < len(self.concentric_circle_radii): | |
456 | self.cornerCallback("all") | |
457 | elif self.corner is not None: | |
458 | if self.cornerCallback: | |
459 | self.lastCorner = self.corner | |
460 | self.lastMove = None | |
461 | self.cornerCallback(self.corner_to_axis[self.corner]) | |
462 | ||
463 | def drawCorner(self, gc, x, y, angle = 0.0): | |
464 | w, h = self.corner_size | |
465 | ||
466 | gc.PushState() | |
467 | gc.Translate(x, y) | |
468 | gc.Rotate(angle) | |
469 | path = gc.CreatePath() | |
470 | path.MoveToPoint(-w / 2, -h / 2) | |
471 | path.AddLineToPoint(w / 2, -h / 2) | |
472 | path.AddLineToPoint(w / 2, -h / 2 + h / 4) | |
473 | path.AddArc(w / 2, h / 2, self.outer_radius, 3 * math.pi / 2, math.pi, False) | |
474 | path.AddLineToPoint(-w / 2, h / 2) | |
475 | path.AddLineToPoint(-w / 2, -h / 2) | |
476 | gc.DrawPath(path) | |
477 | gc.PopState() | |
478 | ||
479 | def draw(self, dc, w, h): | |
480 | dc.SetBackground(wx.Brush(self.bgcolor)) | |
481 | dc.Clear() | |
482 | gc = wx.GraphicsContext.Create(dc) | |
483 | ||
484 | if self.bg_bmp: | |
485 | w, h = (self.bg_bmp.GetWidth(), self.bg_bmp.GetHeight()) | |
486 | gc.DrawBitmap(self.bg_bmp, 0, 0, w, h) | |
487 | ||
488 | if self.enabled and self.IsEnabled(): | |
489 | # Brush and pen for grey overlay when mouse hovers over | |
490 | gc.SetPen(wx.Pen(wx.Colour(100, 100, 100, 172), 4)) | |
491 | gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 128))) | |
492 | ||
493 | if self.concentric is not None: | |
494 | if self.concentric < len(self.concentric_circle_radii): | |
495 | self.drawCenteredDisc(gc, self.concentric_circle_radii[-1]) | |
496 | elif self.corner is not None: | |
497 | self.highlightCorner(gc, self.corner) | |
498 | else: | |
499 | gc.SetPen(wx.Pen(self.bgcolor, 0)) | |
500 | gc.SetBrush(wx.Brush(self.bgcolormask)) | |
501 | gc.DrawRectangle(0, 0, w, h) |