1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
10 import cStringIO as StringIO
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
18 from Cura.gui.util import webcam
20 DESIGN_FILE_EXT = ['.scad', '.blend', '.max', '.stp', '.step', '.igs', '.iges', '.sldasm', '.sldprt', '.skp', '.iam', '.prt', '.x_t', '.ipt', '.dwg', '.123d', '.wings', '.fcstd', '.top']
22 def getClipboardText():
25 if not wx.TheClipboard.IsOpened():
26 wx.TheClipboard.Open()
27 do = wx.TextDataObject()
28 if wx.TheClipboard.GetData(do):
30 wx.TheClipboard.Close()
35 def getAdditionalFiles(objects, onlyExtChanged):
38 name = obj.getOriginFilename()
41 names.add(name[:name.rfind('.')])
44 for ext in DESIGN_FILE_EXT:
45 if os.path.isfile(name + ext):
46 ret.append(name + ext)
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:
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)
65 self._indicatorWindow = workingIndicatorWindow(self._mainWindow)
66 self._getAuthorizationWindow = getAuthorizationWindow(self._mainWindow, self._ym)
67 self._newDesignWindow = newDesignWindow(self._mainWindow, self, self._ym)
69 thread = threading.Thread(target=self.checkAuthorizationThread)
73 def _progressCallback(self, progress):
74 self._indicatorWindow.progress(progress)
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)
84 wx.CallAfter(self._getAuthorizationWindow.Show)
85 lastTriedClipboard = ''
86 while not self._ym.isAuthorized():
88 if self._getAuthorizationWindow.abort:
89 wx.CallAfter(self._getAuthorizationWindow.Destroy)
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)
102 #TODO: Would you like to create a new design or add the model to an existing design?
103 wx.CallAfter(self._newDesignWindow.Show)
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))
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)
115 wx.CallAfter(wx.MessageBox, _("Failed to create a design, nothing uploaded!"), _("YouMagine error."), wx.OK | wx.ICON_ERROR)
118 for obj in self._scene.objects():
119 wx.CallAfter(self._indicatorWindow.showBusy, _("Building model %s...") % (obj.getName()))
121 s = StringIO.StringIO()
122 filename = obj.getName()
123 if obj.canStoreAsSTL():
124 stl.saveSceneStream(s, [obj])
127 amf.saveSceneStream(s, filename, [obj])
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)
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)
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)
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.
161 self._ym.publishDesign(id)
162 wx.CallAfter(self._indicatorWindow.Hide)
164 webbrowser.open(self._ym.viewUrlForDesign(id))
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)
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'))
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))
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)
194 self._busyTimer = wx.Timer(self)
195 self.Bind(wx.EVT_TIMER, self._busyUpdate, self._busyTimer)
196 self._busyTimer.Start(100)
198 def _busyUpdate(self, e):
199 if self._busyState is None:
202 if self._busyState >= len(self._busyBitmaps):
204 self._indicatorBitmap.SetBitmap(self._busyBitmaps[self._busyState])
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)
212 def showBusy(self, text):
213 self._statusText.SetLabel(text)
214 self._progress.Hide()
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)
229 self._requestButton = wx.Button(self._panel, -1, _("Request authorization from YouMagine"))
230 self._authToken = wx.TextCtrl(self._panel, -1, _("Paste token here"))
232 self._panel._sizer = wx.GridBagSizer(5, 5)
233 self._panel.SetSizer(self._panel._sizer)
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)
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)
250 self._authToken.SetFocus()
251 self._authToken.SelectAll()
253 def OnRequestAuthorization(self, e):
254 webbrowser.open(self._ym.getAuthorizationUrl())
256 def OnEnterToken(self, e):
257 self._ym.setAuthToken(self._authToken.GetValue())
259 def OnClose(self, e):
262 class newDesignWindow(wx.Frame):
263 def __init__(self, parent, manager, ym):
264 super(newDesignWindow, self).__init__(parent, title='Share on YouMagine')
266 self.SetSizer(wx.BoxSizer())
267 self.GetSizer().Add(p, 1, wx.EXPAND)
268 self._manager = manager
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())
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)))
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"))
300 s = wx.GridBagSizer(5, 5)
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)
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)
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)
333 self._designDescription.SetMinSize((1,1))
334 self._designName.SetFocus()
335 self._designName.SelectAll()
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()
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()
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())
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)
363 def OnTakeImage(self, e):
364 w = webcamPhotoWindow(self)
369 wx.MessageBox(_("No webcam found on your system"), _("Webcam error"), wx.OK | wx.ICON_ERROR)
371 def _addImage(self, image):
373 if type(image) in types.StringTypes:
375 wxImage = wx.ImageFromBitmap(wx.Bitmap(image))
379 wxImage = wx.ImageFromBitmap(image)
383 width, height = wxImage.GetSize()
385 height = height*70/width
388 width = width*52/height
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
396 ctrl.imageData = image
398 delButton = wx.Button(ctrl, -1, 'X', style=wx.BU_EXACTFIT)
399 self.Bind(wx.EVT_BUTTON, self.OnDeleteImage, delButton)
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)
406 def OnDeleteImage(self, e):
407 ctrl = e.GetEventObject().GetParent()
408 self._imageScroll.GetSizer().Detach(ctrl)
411 self._imageScroll.Layout()
412 self._imageScroll.Refresh()
413 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
415 class webcamPhotoWindow(wx.Frame):
416 def __init__(self, parent):
417 super(webcamPhotoWindow, self).__init__(parent, title='YouMagine')
420 self.SetSizer(wx.BoxSizer())
421 self.GetSizer().Add(p, 1, wx.EXPAND)
423 self._cam = webcam.webcam()
424 self._cam.takeNewImage(False)
426 s = wx.GridBagSizer(3, 3)
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)
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)
438 if self._cam.getLastImage() is not None:
439 self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
441 self._preview.SetMinSize((640, 480))
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)
451 self._takeImageTimer.Start(200)
454 return self._cam.hasCamera()
456 def OnCameraChange(self, e):
457 self._cam.setActiveCamera(self._cameraSelect.GetSelection())
459 def OnTakeImage(self, e):
460 self.GetParent()._addImage(self._cam.getLastImage())
463 def OnTakeImageTimer(self, e):
464 self._cam.takeNewImage(False)
467 def OnCameraEraseBackground(self, e):
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()))
477 dc.DrawBitmap(self._cam.getLastImage(), 0, 0)