chiark / gitweb /
Handle webcam support a bit better, show an error when no webcam is found, and not...
[cura.git] / Cura / gui / tools / youmagineGui.py
1 from __future__ import absolute_import
2 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
3
4 import wx
5 import threading
6 import time
7 import re
8 import os
9 import types
10 import webbrowser
11 import cStringIO as StringIO
12
13 from Cura.util import profile
14 from Cura.util import youmagine
15 from Cura.util.meshLoaders import stl
16 from Cura.util.meshLoaders import amf
17 from Cura.util.resources import getPathForImage
18
19 from Cura.gui.util import webcam
20
21 DESIGN_FILE_EXT = ['.scad', '.blend', '.max', '.stp', '.step', '.igs', '.iges', '.sldasm', '.sldprt', '.skp', '.iam', '.prt', '.x_t', '.ipt', '.dwg', '.123d', '.wings', '.fcstd', '.top']
22
23 def getClipboardText():
24         ret = ''
25         try:
26                 if not wx.TheClipboard.IsOpened():
27                         wx.TheClipboard.Open()
28                         do = wx.TextDataObject()
29                         if wx.TheClipboard.GetData(do):
30                                 ret = do.GetText()
31                         wx.TheClipboard.Close()
32                 return ret
33         except:
34                 return ret
35
36 def getAdditionalFiles(objects, onlyExtChanged):
37         names = set()
38         for obj in objects:
39                 name = obj.getOriginFilename()
40                 if name is None:
41                         continue
42                 names.add(name[:name.rfind('.')])
43         ret = []
44         for name in names:
45                 for ext in DESIGN_FILE_EXT:
46                         if os.path.isfile(name + ext):
47                                 ret.append(name + ext)
48         if onlyExtChanged:
49                 return ret
50         onlyExtList = ret
51         ret = []
52         for name in names:
53                 for ext in DESIGN_FILE_EXT:
54                         for filename in os.listdir(os.path.dirname(name)):
55                                 filename = os.path.join(os.path.dirname(name), filename)
56                                 if filename.endswith(ext) and filename not in ret and filename not in onlyExtList:
57                                         ret.append(filename)
58         return ret
59
60 class youmagineManager(object):
61         def __init__(self, parent, objectScene):
62                 self._mainWindow = parent
63                 self._scene = objectScene
64                 self._ym = youmagine.Youmagine(profile.getPreference('youmagine_token'), self._progressCallback)
65
66                 self._indicatorWindow = workingIndicatorWindow(self._mainWindow)
67                 self._getAuthorizationWindow = getAuthorizationWindow(self._mainWindow, self._ym)
68                 self._newDesignWindow = newDesignWindow(self._mainWindow, self, self._ym)
69
70                 thread = threading.Thread(target=self.checkAuthorizationThread)
71                 thread.daemon = True
72                 thread.start()
73
74         def _progressCallback(self, progress):
75                 self._indicatorWindow.progress(progress)
76
77         #Do all the youmagine communication in a background thread, because it can take a while and block the UI thread otherwise
78         def checkAuthorizationThread(self):
79                 wx.CallAfter(self._indicatorWindow.showBusy, _("Checking token"))
80                 if not self._ym.isAuthorized():
81                         wx.CallAfter(self._indicatorWindow.Hide)
82                         if not self._ym.isHostReachable():
83                                 wx.MessageBox(_("Failed to contact YouMagine.com"), _("YouMagine error."), wx.OK | wx.ICON_ERROR)
84                                 return
85                         wx.CallAfter(self._getAuthorizationWindow.Show)
86                         lastTriedClipboard = ''
87                         while not self._ym.isAuthorized():
88                                 time.sleep(0.1)
89                                 if self._getAuthorizationWindow.abort:
90                                         wx.CallAfter(self._getAuthorizationWindow.Destroy)
91                                         return
92                                 clipboard = getClipboardText()
93                                 if len(clipboard) == 20:
94                                         if clipboard != lastTriedClipboard and re.match('[a-zA-Z0-9]*', clipboard):
95                                                 lastTriedClipboard = clipboard
96                                                 self._ym.setAuthToken(clipboard)
97                         profile.putPreference('youmagine_token', self._ym.getAuthToken())
98                         wx.CallAfter(self._getAuthorizationWindow.Hide)
99                         wx.CallAfter(self._getAuthorizationWindow.Destroy)
100                         wx.MessageBox(_("Cura is now authorized to share on YouMagine"), _("YouMagine."), wx.OK | wx.ICON_INFORMATION)
101                 wx.CallAfter(self._indicatorWindow.Hide)
102
103                 #TODO: Would you like to create a new design or add the model to an existing design?
104                 wx.CallAfter(self._newDesignWindow.Show)
105
106         def createNewDesign(self, name, description, category, license, imageList, extraFileList, publish):
107                 thread = threading.Thread(target=self.createNewDesignThread, args=(name, description, category, license, imageList, extraFileList, publish))
108                 thread.daemon = True
109                 thread.start()
110
111         def createNewDesignThread(self, name, description, category, license, imageList, extraFileList, publish):
112                 wx.CallAfter(self._indicatorWindow.showBusy, _("Creating new design on YouMagine..."))
113                 id = self._ym.createDesign(name, description, category, license)
114                 wx.CallAfter(self._indicatorWindow.Hide)
115                 if id is None:
116                         wx.MessageBox(_("Failed to create a design, nothing uploaded!"), _("YouMagine error."), wx.OK | wx.ICON_ERROR)
117                         return
118
119                 for obj in self._scene.objects():
120                         wx.CallAfter(self._indicatorWindow.showBusy, _("Building model %s...") % (obj.getName()))
121                         time.sleep(0.1)
122                         s = StringIO.StringIO()
123                         filename = obj.getName()
124                         if obj.canStoreAsSTL():
125                                 stl.saveSceneStream(s, [obj])
126                                 filename += '.stl'
127                         else:
128                                 amf.saveSceneStream(s, filename, [obj])
129                                 filename += '.amf'
130
131                         wx.CallAfter(self._indicatorWindow.showBusy, _("Uploading model %s...") % (filename))
132                         if self._ym.createDocument(id, filename, s.getvalue()) is None:
133                                 wx.MessageBox(_("Failed to upload %s!") % (filename), _("YouMagine error."), wx.OK | wx.ICON_ERROR)
134                         s.close()
135
136                 for extra in extraFileList:
137                         wx.CallAfter(self._indicatorWindow.showBusy, _("Uploading file %s...") % (os.path.basename(extra)))
138                         with open(extra, "rb") as f:
139                                 if self._ym.createDocument(id, os.path.basename(extra), f.read()) is None:
140                                         wx.MessageBox(_("Failed to upload %s!") % (os.path.basename(extra)), _("YouMagine error."), wx.OK | wx.ICON_ERROR)
141
142                 for image in imageList:
143                         if type(image) in types.StringTypes:
144                                 filename = os.path.basename(image)
145                                 wx.CallAfter(self._indicatorWindow.showBusy, _("Uploading image %s...") % (filename))
146                                 with open(image, "rb") as f:
147                                         if self._ym.createImage(id, filename, f.read()) is None:
148                                                 wx.MessageBox(_("Failed to upload %s!") % (filename), _("YouMagine error."), wx.OK | wx.ICON_ERROR)
149                         elif type(image) is wx.Bitmap:
150                                 s = StringIO.StringIO()
151                                 if wx.ImageFromBitmap(image).SaveStream(s, wx.BITMAP_TYPE_JPEG):
152                                         if self._ym.createImage(id, "snapshot.jpg", s.getvalue()) is None:
153                                                 wx.MessageBox(_("Failed to upload snapshot!"), _("YouMagine error."), wx.OK | wx.ICON_ERROR)
154                         else:
155                                 print type(image)
156
157                 if publish:
158                         wx.CallAfter(self._indicatorWindow.showBusy, _("Publishing design..."))
159                         if not self._ym.publishDesign(id):
160                                 #If publishing failed try again after 1 second, this might help when you need to wait for the renderer. But does not always work.
161                                 time.sleep(1)
162                                 self._ym.publishDesign(id)
163                 wx.CallAfter(self._indicatorWindow.Hide)
164
165                 webbrowser.open(self._ym.viewUrlForDesign(id))
166
167
168 class workingIndicatorWindow(wx.Frame):
169         def __init__(self, parent):
170                 super(workingIndicatorWindow, self).__init__(parent, title='YouMagine', style=wx.FRAME_TOOL_WINDOW|wx.FRAME_FLOAT_ON_PARENT|wx.FRAME_NO_TASKBAR|wx.CAPTION)
171                 self._panel = wx.Panel(self)
172                 self.SetSizer(wx.BoxSizer())
173                 self.GetSizer().Add(self._panel, 1, wx.EXPAND)
174
175                 self._busyBitmaps = [
176                         wx.Bitmap(getPathForImage('busy-0.png')),
177                         wx.Bitmap(getPathForImage('busy-1.png')),
178                         wx.Bitmap(getPathForImage('busy-2.png')),
179                         wx.Bitmap(getPathForImage('busy-3.png'))
180                 ]
181
182                 self._indicatorBitmap = wx.StaticBitmap(self._panel, -1, wx.EmptyBitmapRGBA(24, 24, red=255, green=255, blue=255, alpha=1))
183                 self._statusText = wx.StaticText(self._panel, -1, '...')
184                 self._progress = wx.Gauge(self._panel, -1)
185                 self._progress.SetRange(1000)
186                 self._progress.SetMinSize((250, 30))
187
188                 self._panel._sizer = wx.GridBagSizer(2, 2)
189                 self._panel.SetSizer(self._panel._sizer)
190                 self._panel._sizer.Add(self._indicatorBitmap, (0, 0), flag=wx.ALL, border=5)
191                 self._panel._sizer.Add(self._statusText, (0, 1), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border=5)
192                 self._panel._sizer.Add(self._progress, (1, 0), span=(1,2), flag=wx.EXPAND|wx.ALL, border=5)
193
194                 self._busyState = 0
195                 self._busyTimer = wx.Timer(self)
196                 self.Bind(wx.EVT_TIMER, self._busyUpdate, self._busyTimer)
197                 self._busyTimer.Start(100)
198
199         def _busyUpdate(self, e):
200                 if self._busyState is None:
201                         return
202                 self._busyState += 1
203                 if self._busyState >= len(self._busyBitmaps):
204                         self._busyState = 0
205                 self._indicatorBitmap.SetBitmap(self._busyBitmaps[self._busyState])
206
207         def progress(self, progressAmount):
208                 wx.CallAfter(self._progress.Show)
209                 wx.CallAfter(self._progress.SetValue, progressAmount*1000)
210                 wx.CallAfter(self.Layout)
211                 wx.CallAfter(self.Fit)
212
213         def showBusy(self, text):
214                 self._statusText.SetLabel(text)
215                 self._progress.Hide()
216                 self.Layout()
217                 self.Fit()
218                 self.Centre()
219                 self.Show()
220
221 class getAuthorizationWindow(wx.Frame):
222         def __init__(self, parent, ym):
223                 super(getAuthorizationWindow, self).__init__(parent, title='YouMagine')
224                 self._panel = wx.Panel(self)
225                 self.SetSizer(wx.BoxSizer())
226                 self.GetSizer().Add(self._panel, 1, wx.EXPAND)
227                 self._ym = ym
228                 self.abort = False
229
230                 self._requestButton = wx.Button(self._panel, -1, _("Request authorization from YouMagine"))
231                 self._authToken = wx.TextCtrl(self._panel, -1, _("Paste token here"))
232
233                 self._panel._sizer = wx.GridBagSizer(5, 5)
234                 self._panel.SetSizer(self._panel._sizer)
235
236                 self._panel._sizer.Add(wx.StaticBitmap(self._panel, -1, wx.Bitmap(getPathForImage('youmagine-text.png'))), (0,0), span=(1,4), flag=wx.ALIGN_CENTRE | wx.ALL, border=5)
237                 self._panel._sizer.Add(wx.StaticText(self._panel, -1, _("To share your designs on YouMagine\nyou need an account on YouMagine.com\nand authorize Cura to access your account.")), (1, 1))
238                 self._panel._sizer.Add(self._requestButton, (2, 1), flag=wx.ALL)
239                 self._panel._sizer.Add(wx.StaticText(self._panel, -1, _("This will open a browser window where you can\nauthorize Cura to access your YouMagine account.\nYou can revoke access at any time\nfrom YouMagine.com")), (3, 1), flag=wx.ALL)
240                 self._panel._sizer.Add(wx.StaticLine(self._panel, -1), (4,0), span=(1,4), flag=wx.EXPAND | wx.ALL)
241                 self._panel._sizer.Add(self._authToken, (5, 1), flag=wx.EXPAND | wx.ALL)
242                 self._panel._sizer.Add(wx.StaticLine(self._panel, -1), (6,0), span=(1,4), flag=wx.EXPAND | wx.ALL)
243
244                 self.Bind(wx.EVT_BUTTON, self.OnRequestAuthorization, self._requestButton)
245                 self.Bind(wx.EVT_TEXT, self.OnEnterToken, self._authToken)
246                 self.Bind(wx.EVT_CLOSE, self.OnClose)
247
248                 self.Fit()
249                 self.Centre()
250
251                 self._authToken.SetFocus()
252                 self._authToken.SelectAll()
253
254         def OnRequestAuthorization(self, e):
255                 webbrowser.open(self._ym.getAuthorizationUrl())
256
257         def OnEnterToken(self, e):
258                 self._ym.setAuthToken(self._authToken.GetValue())
259
260         def OnClose(self, e):
261                 self.abort = True
262
263 class newDesignWindow(wx.Frame):
264         def __init__(self, parent, manager, ym):
265                 super(newDesignWindow, self).__init__(parent, title='Share on YouMagine')
266                 p = wx.Panel(self)
267                 self.SetSizer(wx.BoxSizer())
268                 self.GetSizer().Add(p, 1, wx.EXPAND)
269                 self._manager = manager
270                 self._ym = ym
271
272                 categoryOptions = ym.getCategories()
273                 licenseOptions = ym.getLicenses()
274                 self._designName = wx.TextCtrl(p, -1, _("Design name"))
275                 self._designDescription = wx.TextCtrl(p, -1, '', size=(1, 150), style = wx.TE_MULTILINE)
276                 self._designLicense = wx.ComboBox(p, -1, licenseOptions[0], choices=licenseOptions, style=wx.CB_DROPDOWN|wx.CB_READONLY)
277                 self._category = wx.ComboBox(p, -1, categoryOptions[-1], choices=categoryOptions, style=wx.CB_DROPDOWN|wx.CB_READONLY)
278                 self._publish = wx.CheckBox(p, -1, _("Publish after upload"))
279                 self._shareButton = wx.Button(p, -1, _("Share!"))
280                 self._imageScroll = wx.lib.scrolledpanel.ScrolledPanel(p)
281                 self._additionalFiles = wx.CheckListBox(p, -1)
282                 self._additionalFiles.InsertItems(getAdditionalFiles(self._manager._scene.objects(), True), 0)
283                 self._additionalFiles.SetChecked(range(0, self._additionalFiles.GetCount()))
284                 self._additionalFiles.InsertItems(getAdditionalFiles(self._manager._scene.objects(), False), self._additionalFiles.GetCount())
285
286                 self._imageScroll.SetSizer(wx.BoxSizer(wx.HORIZONTAL))
287                 self._addImageButton = wx.Button(self._imageScroll, -1, _("Add..."), size=(70,52))
288                 self._imageScroll.GetSizer().Add(self._addImageButton)
289                 self._snapshotButton = wx.Button(self._imageScroll, -1, _("Webcam..."), size=(70,52))
290                 self._imageScroll.GetSizer().Add(self._snapshotButton)
291                 if not webcam.hasWebcamSupport():
292                         self._snapshotButton.Hide()
293                 self._imageScroll.Fit()
294                 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
295                 self._imageScroll.SetMinSize((20, self._imageScroll.GetSize()[1] + wx.SystemSettings_GetMetric(wx.SYS_HSCROLL_Y)))
296
297                 self._publish.SetValue(True)
298                 self._publish.SetToolTipString(
299                         _("Directly publish the design after uploading.\nWithout this check the design will not be public\nuntil you publish it yourself on YouMagine.com"))
300
301                 s = wx.GridBagSizer(5, 5)
302                 p.SetSizer(s)
303
304                 s.Add(wx.StaticBitmap(p, -1, wx.Bitmap(getPathForImage('youmagine-text.png'))), (0,0), span=(1,3), flag=wx.ALIGN_CENTRE | wx.ALL, border=5)
305                 s.Add(wx.StaticText(p, -1, _("Design name:")), (1, 0), flag=wx.LEFT|wx.TOP, border=5)
306                 s.Add(self._designName, (1, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
307                 s.Add(wx.StaticText(p, -1, _("Description:")), (2, 0), flag=wx.LEFT|wx.TOP, border=5)
308                 s.Add(self._designDescription, (2, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
309                 s.Add(wx.StaticText(p, -1, _("Category:")), (3, 0), flag=wx.LEFT|wx.TOP, border=5)
310                 s.Add(self._category, (3, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
311                 s.Add(wx.StaticText(p, -1, _("License:")), (4, 0), flag=wx.LEFT|wx.TOP, border=5)
312                 s.Add(self._designLicense, (4, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
313                 s.Add(wx.StaticLine(p, -1), (5,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
314                 s.Add(wx.StaticText(p, -1, _("Images:")), (6, 0), flag=wx.LEFT|wx.TOP, border=5)
315                 s.Add(self._imageScroll, (6, 1), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
316                 s.Add(wx.StaticLine(p, -1), (7,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
317                 s.Add(wx.StaticText(p, -1, _("Related design files:")), (8, 0), flag=wx.LEFT|wx.TOP, border=5)
318
319                 s.Add(self._additionalFiles, (8, 1), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
320                 s.Add(wx.StaticLine(p, -1), (9,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
321                 s.Add(self._shareButton, (10, 1), flag=wx.BOTTOM, border=15)
322                 s.Add(self._publish, (10, 2), flag=wx.BOTTOM|wx.ALIGN_CENTER_VERTICAL, border=15)
323
324                 s.AddGrowableRow(2)
325                 s.AddGrowableCol(2)
326
327                 self.Bind(wx.EVT_BUTTON, self.OnShare, self._shareButton)
328                 self.Bind(wx.EVT_BUTTON, self.OnAddImage, self._addImageButton)
329                 self.Bind(wx.EVT_BUTTON, self.OnTakeImage, self._snapshotButton)
330
331                 self.Fit()
332                 self.Centre()
333
334                 self._designDescription.SetMinSize((1,1))
335                 self._designName.SetFocus()
336                 self._designName.SelectAll()
337
338         def OnShare(self, e):
339                 if self._designName.GetValue() == '':
340                         wx.MessageBox(_("The name cannot be empty"), _("New design error."), wx.OK | wx.ICON_ERROR)
341                         self._designName.SetFocus()
342                         return
343                 if self._designDescription.GetValue() == '':
344                         wx.MessageBox(_("The description cannot be empty"), _("New design error."), wx.OK | wx.ICON_ERROR)
345                         self._designDescription.SetFocus()
346                         return
347                 imageList = []
348                 for child in self._imageScroll.GetChildren():
349                         if hasattr(child, 'imageFilename'):
350                                 imageList.append(child.imageFilename)
351                         if hasattr(child, 'imageData'):
352                                 imageList.append(child.imageData)
353                 self._manager.createNewDesign(self._designName.GetValue(), self._designDescription.GetValue(), self._category.GetValue(), self._designLicense.GetValue(), imageList, self._additionalFiles.GetCheckedStrings(), self._publish.GetValue())
354                 self.Destroy()
355
356         def OnAddImage(self, e):
357                 dlg=wx.FileDialog(self, "Select image file...", style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE)
358                 dlg.SetWildcard("Image files (*.jpg,*.jpeg,*.png)|*.jpg;*.jpeg;*.png")
359                 if dlg.ShowModal() == wx.ID_OK:
360                         for filename in dlg.GetPaths():
361                                 self._addImage(filename)
362                 dlg.Destroy()
363
364         def OnTakeImage(self, e):
365                 w = webcamPhotoWindow(self)
366                 if w.hasCamera():
367                         w.Show()
368                 else:
369                         w.Destroy()
370                         wx.MessageBox(_("No webcam found on your system"), _("Webcam error"), wx.OK | wx.ICON_ERROR)
371
372         def _addImage(self, image):
373                 wxImage = None
374                 if type(image) in types.StringTypes:
375                         try:
376                                 wxImage = wx.ImageFromBitmap(wx.Bitmap(image))
377                         except:
378                                 pass
379                 else:
380                         wxImage = wx.ImageFromBitmap(image)
381                 if wxImage is None:
382                         return
383
384                 width, height = wxImage.GetSize()
385                 if width > 70:
386                         height = height*70/width
387                         width = 70
388                 if height > 52:
389                         width = width*52/height
390                         height = 52
391                 wxImage.Rescale(width, height, wx.IMAGE_QUALITY_NORMAL)
392                 wxImage.Resize((70, 52), ((70-width)/2, (52-height)/2))
393                 ctrl = wx.StaticBitmap(self._imageScroll, -1, wx.BitmapFromImage(wxImage))
394                 if type(image) in types.StringTypes:
395                         ctrl.imageFilename = image
396                 else:
397                         ctrl.imageData = image
398
399                 delButton = wx.Button(ctrl, -1, 'X', style=wx.BU_EXACTFIT)
400                 self.Bind(wx.EVT_BUTTON, self.OnDeleteImage, delButton)
401
402                 self._imageScroll.GetSizer().Insert(len(self._imageScroll.GetChildren())-3, ctrl)
403                 self._imageScroll.Layout()
404                 self._imageScroll.Refresh()
405                 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
406
407         def OnDeleteImage(self, e):
408                 ctrl = e.GetEventObject().GetParent()
409                 self._imageScroll.GetSizer().Detach(ctrl)
410                 ctrl.Destroy()
411
412                 self._imageScroll.Layout()
413                 self._imageScroll.Refresh()
414                 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
415
416 class webcamPhotoWindow(wx.Frame):
417         def __init__(self, parent):
418                 super(webcamPhotoWindow, self).__init__(parent, title='YouMagine')
419                 p = wx.Panel(self)
420                 self.panel = p
421                 self.SetSizer(wx.BoxSizer())
422                 self.GetSizer().Add(p, 1, wx.EXPAND)
423
424                 self._cam = webcam.webcam()
425                 self._cam.takeNewImage(False)
426
427                 s = wx.GridBagSizer(3, 3)
428                 p.SetSizer(s)
429
430                 self._preview = wx.Panel(p)
431                 self._cameraSelect = wx.ComboBox(p, -1, self._cam.listCameras()[0], choices=self._cam.listCameras(), style=wx.CB_DROPDOWN|wx.CB_READONLY)
432                 self._takeImageButton = wx.Button(p, -1, 'Snap image')
433                 self._takeImageTimer = wx.Timer(self)
434
435                 s.Add(self._takeImageButton, pos=(1, 0), flag=wx.ALL, border=5)
436                 s.Add(self._cameraSelect, pos=(1, 1), flag=wx.ALL, border=5)
437                 s.Add(self._preview, pos=(0, 0), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=5)
438
439                 if self._cam.getLastImage() is not None:
440                         self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
441                 else:
442                         self._preview.SetMinSize((640, 480))
443
444                 self._preview.Bind(wx.EVT_ERASE_BACKGROUND, self.OnCameraEraseBackground)
445                 self.Bind(wx.EVT_BUTTON, self.OnTakeImage, self._takeImageButton)
446                 self.Bind(wx.EVT_TIMER, self.OnTakeImageTimer, self._takeImageTimer)
447                 self.Bind(wx.EVT_COMBOBOX, self.OnCameraChange, self._cameraSelect)
448
449                 self.Fit()
450                 self.Centre()
451
452                 self._takeImageTimer.Start(200)
453
454         def hasCamera(self):
455                 return self._cam.hasCamera()
456
457         def OnCameraChange(self, e):
458                 self._cam.setActiveCamera(self._cameraSelect.GetSelection())
459
460         def OnTakeImage(self, e):
461                 self.GetParent()._addImage(self._cam.getLastImage())
462                 self.Destroy()
463
464         def OnTakeImageTimer(self, e):
465                 self._cam.takeNewImage(False)
466                 self.Refresh()
467
468         def OnCameraEraseBackground(self, e):
469                 dc = e.GetDC()
470                 if not dc:
471                         dc = wx.ClientDC(self)
472                         rect = self.GetUpdateRegion().GetBox()
473                         dc.SetClippingRect(rect)
474                 dc.SetBackground(wx.Brush(self._preview.GetBackgroundColour(), wx.SOLID))
475                 if self._cam.getLastImage() is not None:
476                         self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
477                         self.panel.Fit()
478                         dc.DrawBitmap(self._cam.getLastImage(), 0, 0)
479                 else:
480                         dc.Clear()