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