1 from __future__ import absolute_import
2 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
11 import cStringIO as StringIO
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
19 from Cura.gui.util import webcam
21 DESIGN_FILE_EXT = ['.scad', '.blend', '.max', '.stp', '.step', '.igs', '.iges', '.sldasm', '.sldprt', '.skp', '.iam', '.prt', '.x_t', '.ipt', '.dwg', '.123d', '.wings', '.fcstd', '.top']
23 def getClipboardText():
26 if not wx.TheClipboard.IsOpened():
27 wx.TheClipboard.Open()
28 do = wx.TextDataObject()
29 if wx.TheClipboard.GetData(do):
31 wx.TheClipboard.Close()
36 def getAdditionalFiles(objects, onlyExtChanged):
39 name = obj.getOriginFilename()
42 names.add(name[:name.rfind('.')])
45 for ext in DESIGN_FILE_EXT:
46 if os.path.isfile(name + ext):
47 ret.append(name + ext)
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:
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)
66 self._indicatorWindow = workingIndicatorWindow(self._mainWindow)
67 self._getAuthorizationWindow = getAuthorizationWindow(self._mainWindow, self._ym)
68 self._newDesignWindow = newDesignWindow(self._mainWindow, self, self._ym)
70 thread = threading.Thread(target=self.checkAuthorizationThread)
74 def _progressCallback(self, progress):
75 self._indicatorWindow.progress(progress)
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)
85 wx.CallAfter(self._getAuthorizationWindow.Show)
86 lastTriedClipboard = ''
87 while not self._ym.isAuthorized():
89 if self._getAuthorizationWindow.abort:
90 wx.CallAfter(self._getAuthorizationWindow.Destroy)
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)
103 #TODO: Would you like to create a new design or add the model to an existing design?
104 wx.CallAfter(self._newDesignWindow.Show)
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))
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)
116 wx.MessageBox(_("Failed to create a design, nothing uploaded!"), _("YouMagine error."), wx.OK | wx.ICON_ERROR)
119 for obj in self._scene.objects():
120 wx.CallAfter(self._indicatorWindow.showBusy, _("Building model %s...") % (obj.getName()))
122 s = StringIO.StringIO()
123 filename = obj.getName()
124 if obj.canStoreAsSTL():
125 stl.saveSceneStream(s, [obj])
128 amf.saveSceneStream(s, filename, [obj])
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)
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)
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)
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.
162 self._ym.publishDesign(id)
163 wx.CallAfter(self._indicatorWindow.Hide)
165 webbrowser.open(self._ym.viewUrlForDesign(id))
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)
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'))
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))
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)
195 self._busyTimer = wx.Timer(self)
196 self.Bind(wx.EVT_TIMER, self._busyUpdate, self._busyTimer)
197 self._busyTimer.Start(100)
199 def _busyUpdate(self, e):
200 if self._busyState is None:
203 if self._busyState >= len(self._busyBitmaps):
205 self._indicatorBitmap.SetBitmap(self._busyBitmaps[self._busyState])
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)
213 def showBusy(self, text):
214 self._statusText.SetLabel(text)
215 self._progress.Hide()
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)
230 self._requestButton = wx.Button(self._panel, -1, _("Request authorization from YouMagine"))
231 self._authToken = wx.TextCtrl(self._panel, -1, _("Paste token here"))
233 self._panel._sizer = wx.GridBagSizer(5, 5)
234 self._panel.SetSizer(self._panel._sizer)
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)
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)
251 self._authToken.SetFocus()
252 self._authToken.SelectAll()
254 def OnRequestAuthorization(self, e):
255 webbrowser.open(self._ym.getAuthorizationUrl())
257 def OnEnterToken(self, e):
258 self._ym.setAuthToken(self._authToken.GetValue())
260 def OnClose(self, e):
263 class newDesignWindow(wx.Frame):
264 def __init__(self, parent, manager, ym):
265 super(newDesignWindow, self).__init__(parent, title='Share on YouMagine')
267 self.SetSizer(wx.BoxSizer())
268 self.GetSizer().Add(p, 1, wx.EXPAND)
269 self._manager = manager
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())
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)))
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"))
301 s = wx.GridBagSizer(5, 5)
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)
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)
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)
334 self._designDescription.SetMinSize((1,1))
335 self._designName.SetFocus()
336 self._designName.SelectAll()
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()
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()
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())
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)
364 def OnTakeImage(self, e):
365 w = webcamPhotoWindow(self)
370 wx.MessageBox(_("No webcam found on your system"), _("Webcam error"), wx.OK | wx.ICON_ERROR)
372 def _addImage(self, image):
374 if type(image) in types.StringTypes:
376 wxImage = wx.ImageFromBitmap(wx.Bitmap(image))
380 wxImage = wx.ImageFromBitmap(image)
384 width, height = wxImage.GetSize()
386 height = height*70/width
389 width = width*52/height
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
397 ctrl.imageData = image
399 delButton = wx.Button(ctrl, -1, 'X', style=wx.BU_EXACTFIT)
400 self.Bind(wx.EVT_BUTTON, self.OnDeleteImage, delButton)
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)
407 def OnDeleteImage(self, e):
408 ctrl = e.GetEventObject().GetParent()
409 self._imageScroll.GetSizer().Detach(ctrl)
412 self._imageScroll.Layout()
413 self._imageScroll.Refresh()
414 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
416 class webcamPhotoWindow(wx.Frame):
417 def __init__(self, parent):
418 super(webcamPhotoWindow, self).__init__(parent, title='YouMagine')
421 self.SetSizer(wx.BoxSizer())
422 self.GetSizer().Add(p, 1, wx.EXPAND)
424 self._cam = webcam.webcam()
425 self._cam.takeNewImage(False)
427 s = wx.GridBagSizer(3, 3)
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)
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)
439 if self._cam.getLastImage() is not None:
440 self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
442 self._preview.SetMinSize((640, 480))
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)
452 self._takeImageTimer.Start(200)
455 return self._cam.hasCamera()
457 def OnCameraChange(self, e):
458 self._cam.setActiveCamera(self._cameraSelect.GetSelection())
460 def OnTakeImage(self, e):
461 self.GetParent()._addImage(self._cam.getLastImage())
464 def OnTakeImageTimer(self, e):
465 self._cam.takeNewImage(False)
468 def OnCameraEraseBackground(self, e):
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()))
478 dc.DrawBitmap(self._cam.getLastImage(), 0, 0)