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