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 self._imageScroll.Fit()
292 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
293 self._imageScroll.SetMinSize((20, self._imageScroll.GetSize()[1] + wx.SystemSettings_GetMetric(wx.SYS_HSCROLL_Y)))
295 self._publish.SetValue(True)
296 self._publish.SetToolTipString(
297 _("Directly publish the design after uploading.\nWithout this check the design will not be public\nuntil you publish it yourself on YouMagine.com"))
299 s = wx.GridBagSizer(5, 5)
302 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)
303 s.Add(wx.StaticText(p, -1, _("Design name:")), (1, 0), flag=wx.LEFT|wx.TOP, border=5)
304 s.Add(self._designName, (1, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
305 s.Add(wx.StaticText(p, -1, _("Description:")), (2, 0), flag=wx.LEFT|wx.TOP, border=5)
306 s.Add(self._designDescription, (2, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
307 s.Add(wx.StaticText(p, -1, _("Category:")), (3, 0), flag=wx.LEFT|wx.TOP, border=5)
308 s.Add(self._category, (3, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
309 s.Add(wx.StaticText(p, -1, _("License:")), (4, 0), flag=wx.LEFT|wx.TOP, border=5)
310 s.Add(self._designLicense, (4, 1), span=(1,2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
311 s.Add(wx.StaticLine(p, -1), (5,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
312 s.Add(wx.StaticText(p, -1, _("Images:")), (6, 0), flag=wx.LEFT|wx.TOP, border=5)
313 s.Add(self._imageScroll, (6, 1), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
314 s.Add(wx.StaticLine(p, -1), (7,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
315 s.Add(wx.StaticText(p, -1, _("Related design files:")), (8, 0), flag=wx.LEFT|wx.TOP, border=5)
317 s.Add(self._additionalFiles, (8, 1), span=(1, 2), flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, border=5)
318 s.Add(wx.StaticLine(p, -1), (9,0), span=(1,3), flag=wx.EXPAND|wx.ALL)
319 s.Add(self._shareButton, (10, 1), flag=wx.BOTTOM, border=15)
320 s.Add(self._publish, (10, 2), flag=wx.BOTTOM|wx.ALIGN_CENTER_VERTICAL, border=15)
325 self.Bind(wx.EVT_BUTTON, self.OnShare, self._shareButton)
326 self.Bind(wx.EVT_BUTTON, self.OnAddImage, self._addImageButton)
327 self.Bind(wx.EVT_BUTTON, self.OnTakeImage, self._snapshotButton)
332 self._designDescription.SetMinSize((1,1))
333 self._designName.SetFocus()
334 self._designName.SelectAll()
336 def OnShare(self, e):
337 if self._designName.GetValue() == '':
338 wx.MessageBox(_("The name cannot be empty"), _("New design error."), wx.OK | wx.ICON_ERROR)
339 self._designName.SetFocus()
341 if self._designDescription.GetValue() == '':
342 wx.MessageBox(_("The description cannot be empty"), _("New design error."), wx.OK | wx.ICON_ERROR)
343 self._designDescription.SetFocus()
346 for child in self._imageScroll.GetChildren():
347 if hasattr(child, 'imageFilename'):
348 imageList.append(child.imageFilename)
349 if hasattr(child, 'imageData'):
350 imageList.append(child.imageData)
351 self._manager.createNewDesign(self._designName.GetValue(), self._designDescription.GetValue(), self._category.GetValue(), self._designLicense.GetValue(), imageList, self._additionalFiles.GetCheckedStrings(), self._publish.GetValue())
354 def OnAddImage(self, e):
355 dlg=wx.FileDialog(self, "Select image file...", style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE)
356 dlg.SetWildcard("Image files (*.jpg,*.jpeg,*.png)|*.jpg;*.jpeg;*.png")
357 if dlg.ShowModal() == wx.ID_OK:
358 for filename in dlg.GetPaths():
359 self._addImage(filename)
362 def OnTakeImage(self, e):
363 webcamPhotoWindow(self).Show()
365 def _addImage(self, image):
367 if type(image) in types.StringTypes:
369 wxImage = wx.ImageFromBitmap(wx.Bitmap(image))
373 wxImage = wx.ImageFromBitmap(image)
377 width, height = wxImage.GetSize()
379 height = height*70/width
382 width = width*52/height
384 wxImage.Rescale(width, height, wx.IMAGE_QUALITY_NORMAL)
385 wxImage.Resize((70, 52), ((70-width)/2, (52-height)/2))
386 ctrl = wx.StaticBitmap(self._imageScroll, -1, wx.BitmapFromImage(wxImage))
387 if type(image) in types.StringTypes:
388 ctrl.imageFilename = image
390 ctrl.imageData = image
392 delButton = wx.Button(ctrl, -1, 'X', style=wx.BU_EXACTFIT)
393 self.Bind(wx.EVT_BUTTON, self.OnDeleteImage, delButton)
395 self._imageScroll.GetSizer().Insert(len(self._imageScroll.GetChildren())-3, ctrl)
396 self._imageScroll.Layout()
397 self._imageScroll.Refresh()
398 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
400 def OnDeleteImage(self, e):
401 ctrl = e.GetEventObject().GetParent()
402 self._imageScroll.GetSizer().Detach(ctrl)
405 self._imageScroll.Layout()
406 self._imageScroll.Refresh()
407 self._imageScroll.SetupScrolling(scroll_x=True, scroll_y=False)
409 class webcamPhotoWindow(wx.Frame):
410 def __init__(self, parent):
411 super(webcamPhotoWindow, self).__init__(parent, title='YouMagine')
414 self.SetSizer(wx.BoxSizer())
415 self.GetSizer().Add(p, 1, wx.EXPAND)
417 self._cam = webcam.webcam()
418 self._cam.takeNewImage(False)
420 s = wx.GridBagSizer(3, 3)
423 self._preview = wx.Panel(p)
424 self._cameraSelect = wx.ComboBox(p, -1, self._cam.listCameras()[0], choices=self._cam.listCameras(), style=wx.CB_DROPDOWN|wx.CB_READONLY)
425 self._takeImageButton = wx.Button(p, -1, 'Snap image')
426 self._takeImageTimer = wx.Timer(self)
428 s.Add(self._takeImageButton, pos=(1, 0), flag=wx.ALL, border=5)
429 s.Add(self._cameraSelect, pos=(1, 1), flag=wx.ALL, border=5)
430 s.Add(self._preview, pos=(0, 0), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=5)
432 if self._cam.getLastImage() is not None:
433 self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
435 self._preview.SetMinSize((640, 480))
437 self._preview.Bind(wx.EVT_ERASE_BACKGROUND, self.OnCameraEraseBackground)
438 self.Bind(wx.EVT_BUTTON, self.OnTakeImage, self._takeImageButton)
439 self.Bind(wx.EVT_TIMER, self.OnTakeImageTimer, self._takeImageTimer)
440 self.Bind(wx.EVT_COMBOBOX, self.OnCameraChange, self._cameraSelect)
445 self._takeImageTimer.Start(200)
447 def OnCameraChange(self, e):
448 self._cam.setActiveCamera(self._cameraSelect.GetSelection())
450 def OnTakeImage(self, e):
451 self.GetParent()._addImage(self._cam.getLastImage())
454 def OnTakeImageTimer(self, e):
455 self._cam.takeNewImage(False)
458 def OnCameraEraseBackground(self, e):
461 dc = wx.ClientDC(self)
462 rect = self.GetUpdateRegion().GetBox()
463 dc.SetClippingRect(rect)
464 dc.SetBackground(wx.Brush(self._preview.GetBackgroundColour(), wx.SOLID))
465 if self._cam.getLastImage() is not None:
466 self._preview.SetMinSize((self._cam.getLastImage().GetWidth(), self._cam.getLastImage().GetHeight()))
468 dc.DrawBitmap(self._cam.getLastImage(), 0, 0)