chiark / gitweb /
8b7f26ff02d4ce36e4d3dc8244659ac6d82d51a7
[cura.git] / Cura / gui / mainWindow.py
1 from __future__ import absolute_import
2
3 import wx
4 import os
5 import webbrowser
6
7 from Cura.gui import configBase
8 from Cura.gui import expertConfig
9 from Cura.gui import preview3d
10 from Cura.gui import sliceProgressPanel
11 from Cura.gui import alterationPanel
12 from Cura.gui import pluginPanel
13 from Cura.gui import preferencesDialog
14 from Cura.gui import configWizard
15 from Cura.gui import firmwareInstall
16 from Cura.gui import printWindow
17 from Cura.gui import simpleMode
18 from Cura.gui import projectPlanner
19 from Cura.gui.tools import batchRun
20 from Cura.gui import flatSlicerWindow
21 from Cura.gui.util import dropTarget
22 from Cura.gui.tools import minecraftImport
23 from Cura.util import validators
24 from Cura.util import profile
25 from Cura.util import version
26 from Cura.util import sliceRun
27 from Cura.util import meshLoader
28
29 class mainWindow(wx.Frame):
30         def __init__(self):
31                 super(mainWindow, self).__init__(None, title='Cura Steam Engine BETA - ' + version.getVersion())
32
33                 self.extruderCount = int(profile.getPreference('extruder_amount'))
34
35                 wx.EVT_CLOSE(self, self.OnClose)
36
37                 self.SetDropTarget(dropTarget.FileDropTarget(self.OnDropFiles, meshLoader.supportedExtensions()))
38
39                 self.normalModeOnlyItems = []
40
41                 mruFile = os.path.join(profile.getBasePath(), 'mru_filelist.ini')
42                 self.config = wx.FileConfig(appName="Cura", 
43                                                 localFilename=mruFile,
44                                                 style=wx.CONFIG_USE_LOCAL_FILE)
45                                                 
46                 self.ID_MRU_MODEL1, self.ID_MRU_MODEL2, self.ID_MRU_MODEL3, self.ID_MRU_MODEL4, self.ID_MRU_MODEL5, self.ID_MRU_MODEL6, self.ID_MRU_MODEL7, self.ID_MRU_MODEL8, self.ID_MRU_MODEL9, self.ID_MRU_MODEL10 = [wx.NewId() for line in xrange(10)]
47                 self.modelFileHistory = wx.FileHistory(10, self.ID_MRU_MODEL1)
48                 self.config.SetPath("/ModelMRU")
49                 self.modelFileHistory.Load(self.config)
50
51                 self.ID_MRU_PROFILE1, self.ID_MRU_PROFILE2, self.ID_MRU_PROFILE3, self.ID_MRU_PROFILE4, self.ID_MRU_PROFILE5, self.ID_MRU_PROFILE6, self.ID_MRU_PROFILE7, self.ID_MRU_PROFILE8, self.ID_MRU_PROFILE9, self.ID_MRU_PROFILE10 = [wx.NewId() for line in xrange(10)]
52                 self.profileFileHistory = wx.FileHistory(10, self.ID_MRU_PROFILE1)
53                 self.config.SetPath("/ProfileMRU")
54                 self.profileFileHistory.Load(self.config)
55
56                 self.menubar = wx.MenuBar()
57                 self.fileMenu = wx.Menu()
58                 i = self.fileMenu.Append(-1, 'Load model file...\tCTRL+L')
59                 self.Bind(wx.EVT_MENU, lambda e: self._showModelLoadDialog(1), i)
60                 i = self.fileMenu.Append(-1, 'Prepare print...\tCTRL+R')
61                 self.Bind(wx.EVT_MENU, self.OnSlice, i)
62                 i = self.fileMenu.Append(-1, 'Print...\tCTRL+P')
63                 self.Bind(wx.EVT_MENU, self.OnPrint, i)
64
65                 self.fileMenu.AppendSeparator()
66                 i = self.fileMenu.Append(-1, 'Open Profile...')
67                 self.normalModeOnlyItems.append(i)
68                 self.Bind(wx.EVT_MENU, self.OnLoadProfile, i)
69                 i = self.fileMenu.Append(-1, 'Save Profile...')
70                 self.normalModeOnlyItems.append(i)
71                 self.Bind(wx.EVT_MENU, self.OnSaveProfile, i)
72                 i = self.fileMenu.Append(-1, 'Load Profile from GCode...')
73                 self.normalModeOnlyItems.append(i)
74                 self.Bind(wx.EVT_MENU, self.OnLoadProfileFromGcode, i)
75                 self.fileMenu.AppendSeparator()
76                 i = self.fileMenu.Append(-1, 'Reset Profile to default')
77                 self.normalModeOnlyItems.append(i)
78                 self.Bind(wx.EVT_MENU, self.OnResetProfile, i)
79
80                 self.fileMenu.AppendSeparator()
81                 i = self.fileMenu.Append(-1, 'Preferences...\tCTRL+,')
82                 self.Bind(wx.EVT_MENU, self.OnPreferences, i)
83                 self.fileMenu.AppendSeparator()
84
85                 # Model MRU list
86                 modelHistoryMenu = wx.Menu()
87                 self.fileMenu.AppendMenu(wx.NewId(), "&Recent Model Files", modelHistoryMenu)
88                 self.modelFileHistory.UseMenu(modelHistoryMenu)
89                 self.modelFileHistory.AddFilesToMenu()
90                 self.Bind(wx.EVT_MENU_RANGE, self.OnModelMRU, id=self.ID_MRU_MODEL1, id2=self.ID_MRU_MODEL10)
91
92                 # Profle MRU list
93                 profileHistoryMenu = wx.Menu()
94                 self.fileMenu.AppendMenu(wx.NewId(), "&Recent Profile Files", profileHistoryMenu)
95                 self.profileFileHistory.UseMenu(profileHistoryMenu)
96                 self.profileFileHistory.AddFilesToMenu()
97                 self.Bind(wx.EVT_MENU_RANGE, self.OnProfileMRU, id=self.ID_MRU_PROFILE1, id2=self.ID_MRU_PROFILE10)
98                 
99                 self.fileMenu.AppendSeparator()
100                 i = self.fileMenu.Append(wx.ID_EXIT, 'Quit')
101                 self.Bind(wx.EVT_MENU, self.OnQuit, i)
102                 self.menubar.Append(self.fileMenu, '&File')
103
104                 toolsMenu = wx.Menu()
105                 i = toolsMenu.Append(-1, 'Switch to quickprint...')
106                 self.switchToQuickprintMenuItem = i
107                 self.Bind(wx.EVT_MENU, self.OnSimpleSwitch, i)
108                 i = toolsMenu.Append(-1, 'Switch to full settings...')
109                 self.switchToNormalMenuItem = i
110                 self.Bind(wx.EVT_MENU, self.OnNormalSwitch, i)
111                 toolsMenu.AppendSeparator()
112                 i = toolsMenu.Append(-1, 'Project planner...')
113                 self.Bind(wx.EVT_MENU, self.OnProjectPlanner, i)
114                 self.normalModeOnlyItems.append(i)
115                 i = toolsMenu.Append(-1, 'Batch run...')
116                 self.Bind(wx.EVT_MENU, self.OnBatchRun, i)
117                 self.normalModeOnlyItems.append(i)
118                 #               i = toolsMenu.Append(-1, 'Open SVG (2D) slicer...')
119                 #               self.Bind(wx.EVT_MENU, self.OnSVGSlicerOpen, i)
120                 if minecraftImport.hasMinecraft():
121                         i = toolsMenu.Append(-1, 'Minecraft import...')
122                         self.Bind(wx.EVT_MENU, self.OnMinecraftImport, i)
123                 self.menubar.Append(toolsMenu, 'Tools')
124
125                 expertMenu = wx.Menu()
126                 i = expertMenu.Append(-1, 'Open expert settings...')
127                 self.normalModeOnlyItems.append(i)
128                 self.Bind(wx.EVT_MENU, self.OnExpertOpen, i)
129                 expertMenu.AppendSeparator()
130                 if firmwareInstall.getDefaultFirmware() is not None:
131                         i = expertMenu.Append(-1, 'Install default Marlin firmware')
132                         self.Bind(wx.EVT_MENU, self.OnDefaultMarlinFirmware, i)
133                 i = expertMenu.Append(-1, 'Install custom firmware')
134                 self.Bind(wx.EVT_MENU, self.OnCustomFirmware, i)
135                 expertMenu.AppendSeparator()
136                 i = expertMenu.Append(-1, 'Run first run wizard...')
137                 self.Bind(wx.EVT_MENU, self.OnFirstRunWizard, i)
138                 i = expertMenu.Append(-1, 'Run bed leveling wizard...')
139                 self.Bind(wx.EVT_MENU, self.OnBedLevelWizard, i)
140                 self.menubar.Append(expertMenu, 'Expert')
141
142                 helpMenu = wx.Menu()
143                 i = helpMenu.Append(-1, 'Online documentation...')
144                 self.Bind(wx.EVT_MENU, lambda e: webbrowser.open('http://daid.github.com/Cura'), i)
145                 i = helpMenu.Append(-1, 'Report a problem...')
146                 self.Bind(wx.EVT_MENU, lambda e: webbrowser.open('https://github.com/daid/Cura/issues'), i)
147                 i = helpMenu.Append(-1, 'Check for update...')
148                 self.Bind(wx.EVT_MENU, self.OnCheckForUpdate, i)
149                 self.menubar.Append(helpMenu, 'Help')
150                 self.SetMenuBar(self.menubar)
151
152                 if profile.getPreference('lastFile') != '':
153                         self.filelist = profile.getPreference('lastFile').split(';')
154                         self.SetTitle('Cura - %s - %s' % (version.getVersion(), self.filelist[-1]))
155                 else:
156                         self.filelist = []
157                 self.progressPanelList = []
158
159                 self.splitter = wx.SplitterWindow(self, style = wx.SP_3D | wx.SP_LIVE_UPDATE)
160                 self.leftPane = wx.Panel(self.splitter, style=wx.BORDER_NONE)
161                 self.rightPane = wx.Panel(self.splitter, style=wx.BORDER_NONE)
162                 self.splitter.Bind(wx.EVT_SPLITTER_DCLICK, lambda evt: evt.Veto())
163
164                 ##Gui components##
165                 self.simpleSettingsPanel = simpleMode.simpleModePanel(self.leftPane)
166                 self.normalSettingsPanel = normalSettingsPanel(self.leftPane)
167
168                 self.leftSizer = wx.BoxSizer(wx.VERTICAL)
169                 self.leftSizer.Add(self.simpleSettingsPanel)
170                 self.leftSizer.Add(self.normalSettingsPanel, 1, wx.EXPAND)
171                 self.leftPane.SetSizer(self.leftSizer)
172                 
173                 #Preview window
174                 self.preview3d = preview3d.previewPanel(self.rightPane)
175
176                 #Also bind double clicking the 3D preview to load an STL file.
177                 #self.preview3d.glCanvas.Bind(wx.EVT_LEFT_DCLICK, lambda e: self._showModelLoadDialog(1), self.preview3d.glCanvas)
178
179                 #Main sizer, to position the preview window, buttons and tab control
180                 sizer = wx.BoxSizer()
181                 self.rightPane.SetSizer(sizer)
182                 sizer.Add(self.preview3d, 1, flag=wx.EXPAND)
183
184                 # Main window sizer
185                 sizer = wx.BoxSizer(wx.VERTICAL)
186                 self.SetSizer(sizer)
187                 sizer.Add(self.splitter, 1, wx.EXPAND)
188                 sizer.Layout()
189                 self.sizer = sizer
190
191                 if len(self.filelist) > 0:
192                         self.preview3d.loadModelFiles(self.filelist)
193
194                         # Update the Model MRU
195                         for idx in xrange(0, len(self.filelist)):
196                                 self.addToModelMRU(self.filelist[idx])
197
198                 self.updateProfileToControls()
199
200                 self.SetBackgroundColour(self.normalSettingsPanel.GetBackgroundColour())
201
202                 self.simpleSettingsPanel.Show(False)
203                 self.normalSettingsPanel.Show(False)
204
205                 # Set default window size & position
206                 self.SetSize((wx.Display().GetClientArea().GetWidth()/2,wx.Display().GetClientArea().GetHeight()/2))
207                 self.Centre()
208
209                 # Restore the window position, size & state from the preferences file
210                 self.normalSashPos = 320
211                 try:
212                         if profile.getPreference('window_maximized') == 'True':
213                                 self.Maximize(True)
214                         else:
215                                 posx = int(profile.getPreference('window_pos_x'))
216                                 posy = int(profile.getPreference('window_pos_y'))
217                                 width = int(profile.getPreference('window_width'))
218                                 height = int(profile.getPreference('window_height'))
219                         if posx > 0 or posy > 0:
220                                 self.SetPosition((posx,posy))
221                         if width > 0 and height > 0:
222                                 self.SetSize((width,height))
223                                 
224                         self.normalSashPos = int(profile.getPreference('window_normal_sash'))
225                 except:
226                         self.Maximize(True)
227
228                 self.splitter.SplitVertically(self.leftPane, self.rightPane, self.normalSashPos)
229
230                 self.updateSliceMode()
231
232                 self.Show(True)
233
234         def updateSliceMode(self):
235                 isSimple = profile.getPreference('startMode') == 'Simple'
236
237                 self.normalSettingsPanel.Show(not isSimple)
238                 self.simpleSettingsPanel.Show(isSimple)
239                 self.leftPane.Layout()
240
241                 for i in self.normalModeOnlyItems:
242                         i.Enable(not isSimple)
243                 self.switchToQuickprintMenuItem.Enable(not isSimple)
244                 self.switchToNormalMenuItem.Enable(isSimple)
245
246                 # Set splitter sash position & size
247                 if isSimple:
248                         # Save normal mode sash
249                         self.normalSashPos = self.splitter.GetSashPosition()
250                         
251                         # Change location of sash to width of quick mode pane 
252                         (width, height) = self.simpleSettingsPanel.GetSizer().GetSize() 
253                         self.splitter.SetSashPosition(width, True)
254                         
255                         # Disable sash
256                         self.splitter.SetSashSize(0)
257                 else:
258                         self.splitter.SetSashPosition(self.normalSashPos, True)
259
260                         # Enabled sash
261                         self.splitter.SetSashSize(4)
262                                                                 
263         def OnPreferences(self, e):
264                 prefDialog = preferencesDialog.preferencesDialog(self)
265                 prefDialog.Centre()
266                 prefDialog.Show(True)
267
268         def _showOpenDialog(self, title, wildcard = meshLoader.wildcardFilter()):
269                 dlg=wx.FileDialog(self, title, os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
270                 dlg.SetWildcard(wildcard)
271                 if dlg.ShowModal() == wx.ID_OK:
272                         filename = dlg.GetPath()
273                         dlg.Destroy()
274                         if not(os.path.exists(filename)):
275                                 return False
276                         profile.putPreference('lastFile', filename)
277                         return filename
278                 dlg.Destroy()
279                 return False
280
281         def _showModelLoadDialog(self, amount):
282                 filelist = []
283                 for i in xrange(0, amount):
284                         filelist.append(self._showOpenDialog("Open file to print"))
285                         if filelist[-1] == False:
286                                 return
287                 self._loadModels(filelist)
288
289         def _loadModels(self, filelist):
290                 self.filelist = filelist
291                 self.SetTitle(filelist[-1] + ' - Cura - ' + version.getVersion())
292                 profile.putPreference('lastFile', ';'.join(self.filelist))
293                 self.preview3d.loadModelFiles(self.filelist, True)
294                 self.preview3d.setViewMode("Normal")
295                 
296                 # Update the Model MRU
297                 for idx in xrange(0, len(self.filelist)):
298                         self.addToModelMRU(self.filelist[idx])
299
300         def OnDropFiles(self, files):
301                 self._loadModels(files)
302
303         def OnLoadModel(self, e):
304                 self._showModelLoadDialog(1)
305
306         def OnLoadModel2(self, e):
307                 self._showModelLoadDialog(2)
308
309         def OnLoadModel3(self, e):
310                 self._showModelLoadDialog(3)
311
312         def OnLoadModel4(self, e):
313                 self._showModelLoadDialog(4)
314
315         def OnSlice(self, e):
316                 if len(self.filelist) < 1:
317                         wx.MessageBox('You need to load a file before you can prepare it.', 'Print error', wx.OK | wx.ICON_INFORMATION)
318                         return
319                 isSimple = profile.getPreference('startMode') == 'Simple'
320                 if isSimple:
321                         #save the current profile so we can put it back latter
322                         oldProfile = profile.getGlobalProfileString()
323                         self.simpleSettingsPanel.setupSlice()
324                 #Create a progress panel and add it to the window. The progress panel will start the Skein operation.
325                 spp = sliceProgressPanel.sliceProgressPanel(self, self, self.filelist)
326                 self.sizer.Add(spp, 0, flag=wx.EXPAND)
327                 self.sizer.Layout()
328                 newSize = self.GetSize()
329                 newSize.IncBy(0, spp.GetSize().GetHeight())
330                 if newSize.GetWidth() < wx.GetDisplaySize()[0]:
331                         self.SetSize(newSize)
332                 self.progressPanelList.append(spp)
333                 if isSimple:
334                         profile.loadGlobalProfileFromString(oldProfile)
335
336         def OnPrint(self, e):
337                 if len(self.filelist) < 1:
338                         wx.MessageBox('You need to load a file and prepare it before you can print.', 'Print error', wx.OK | wx.ICON_INFORMATION)
339                         return
340                 if not os.path.exists(sliceRun.getExportFilename(self.filelist[0])):
341                         wx.MessageBox('You need to prepare a print before you can run the actual print.', 'Print error', wx.OK | wx.ICON_INFORMATION)
342                         return
343                 printWindow.printFile(sliceRun.getExportFilename(self.filelist[0]))
344
345         def OnModelMRU(self, e):
346                 fileNum = e.GetId() - self.ID_MRU_MODEL1
347                 path = self.modelFileHistory.GetHistoryFile(fileNum)
348                 # Update Model MRU
349                 self.modelFileHistory.AddFileToHistory(path)  # move up the list
350                 self.config.SetPath("/ModelMRU")
351                 self.modelFileHistory.Save(self.config)
352                 self.config.Flush()
353                 # Load Model
354                 filelist = [ path ]
355                 self._loadModels(filelist)
356
357         def addToModelMRU(self, file):
358                 self.modelFileHistory.AddFileToHistory(file)
359                 self.config.SetPath("/ModelMRU")
360                 self.modelFileHistory.Save(self.config)
361                 self.config.Flush()
362         
363         def OnProfileMRU(self, e):
364                 fileNum = e.GetId() - self.ID_MRU_PROFILE1
365                 path = self.profileFileHistory.GetHistoryFile(fileNum)
366                 # Update Profile MRU
367                 self.profileFileHistory.AddFileToHistory(path)  # move up the list
368                 self.config.SetPath("/ProfileMRU")
369                 self.profileFileHistory.Save(self.config)
370                 self.config.Flush()
371                 # Load Profile  
372                 profile.loadGlobalProfile(path)
373                 self.updateProfileToControls()
374
375         def addToProfileMRU(self, file):
376                 self.profileFileHistory.AddFileToHistory(file)
377                 self.config.SetPath("/ProfileMRU")
378                 self.profileFileHistory.Save(self.config)
379                 self.config.Flush()                     
380
381         def removeSliceProgress(self, spp):
382                 self.progressPanelList.remove(spp)
383                 newSize = self.GetSize()
384                 newSize.IncBy(0, -spp.GetSize().GetHeight())
385                 if newSize.GetWidth() < wx.GetDisplaySize()[0]:
386                         self.SetSize(newSize)
387                 spp.Show(False)
388                 self.sizer.Detach(spp)
389                 self.sizer.Layout()
390
391         def updateProfileToControls(self):
392                 self.preview3d.updateProfileToControls()
393                 self.normalSettingsPanel.updateProfileToControls()
394                 self.simpleSettingsPanel.updateProfileToControls()
395
396         def OnLoadProfile(self, e):
397                 dlg=wx.FileDialog(self, "Select profile file to load", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
398                 dlg.SetWildcard("ini files (*.ini)|*.ini")
399                 if dlg.ShowModal() == wx.ID_OK:
400                         profileFile = dlg.GetPath()
401                         profile.loadGlobalProfile(profileFile)
402                         self.updateProfileToControls()
403
404                         # Update the Profile MRU
405                         self.addToProfileMRU(profileFile)
406                 dlg.Destroy()
407
408         def OnLoadProfileFromGcode(self, e):
409                 dlg=wx.FileDialog(self, "Select gcode file to load profile from", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
410                 dlg.SetWildcard("gcode files (*.gcode)|*.gcode;*.g")
411                 if dlg.ShowModal() == wx.ID_OK:
412                         gcodeFile = dlg.GetPath()
413                         f = open(gcodeFile, 'r')
414                         hasProfile = False
415                         for line in f:
416                                 if line.startswith(';CURA_PROFILE_STRING:'):
417                                         profile.loadGlobalProfileFromString(line[line.find(':')+1:].strip())
418                                         hasProfile = True
419                         if hasProfile:
420                                 self.updateProfileToControls()
421                         else:
422                                 wx.MessageBox('No profile found in GCode file.\nThis feature only works with GCode files made by Cura 12.07 or newer.', 'Profile load error', wx.OK | wx.ICON_INFORMATION)
423                 dlg.Destroy()
424
425         def OnSaveProfile(self, e):
426                 dlg=wx.FileDialog(self, "Select profile file to save", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
427                 dlg.SetWildcard("ini files (*.ini)|*.ini")
428                 if dlg.ShowModal() == wx.ID_OK:
429                         profileFile = dlg.GetPath()
430                         profile.saveGlobalProfile(profileFile)
431                 dlg.Destroy()
432
433         def OnResetProfile(self, e):
434                 dlg = wx.MessageDialog(self, 'This will reset all profile settings to defaults.\nUnless you have saved your current profile, all settings will be lost!\nDo you really want to reset?', 'Profile reset', wx.YES_NO | wx.ICON_QUESTION)
435                 result = dlg.ShowModal() == wx.ID_YES
436                 dlg.Destroy()
437                 if result:
438                         profile.resetGlobalProfile()
439                         self.updateProfileToControls()
440
441         def OnBatchRun(self, e):
442                 br = batchRun.batchRunWindow(self)
443                 br.Centre()
444                 br.Show(True)
445
446         def OnSimpleSwitch(self, e):
447                 profile.putPreference('startMode', 'Simple')
448                 self.updateSliceMode()
449
450         def OnNormalSwitch(self, e):
451                 profile.putPreference('startMode', 'Normal')
452                 self.updateSliceMode()
453
454         def OnDefaultMarlinFirmware(self, e):
455                 firmwareInstall.InstallFirmware()
456
457         def OnCustomFirmware(self, e):
458                 if profile.getPreference('machine_type') == 'ultimaker':
459                         wx.MessageBox('Warning: Installing a custom firmware does not garantee that you machine will function correctly, and could damage your machine.', 'Firmware update', wx.OK | wx.ICON_EXCLAMATION)
460                 dlg=wx.FileDialog(self, "Open firmware to upload", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
461                 dlg.SetWildcard("HEX file (*.hex)|*.hex;*.HEX")
462                 if dlg.ShowModal() == wx.ID_OK:
463                         filename = dlg.GetPath()
464                         if not(os.path.exists(filename)):
465                                 return
466                         #For some reason my Ubuntu 10.10 crashes here.
467                         firmwareInstall.InstallFirmware(filename)
468
469         def OnFirstRunWizard(self, e):
470                 configWizard.configWizard()
471                 self.updateProfileToControls()
472
473         def OnBedLevelWizard(self, e):
474                 configWizard.bedLevelWizard()
475
476         def OnExpertOpen(self, e):
477                 ecw = expertConfig.expertConfigWindow()
478                 ecw.Centre()
479                 ecw.Show(True)
480
481         def OnProjectPlanner(self, e):
482                 pp = projectPlanner.projectPlanner()
483                 pp.Centre()
484                 pp.Show(True)
485
486         def OnMinecraftImport(self, e):
487                 mi = minecraftImport.minecraftImportWindow(self)
488                 mi.Centre()
489                 mi.Show(True)
490
491         def OnSVGSlicerOpen(self, e):
492                 svgSlicer = flatSlicerWindow.flatSlicerWindow()
493                 svgSlicer.Centre()
494                 svgSlicer.Show(True)
495
496         def OnCheckForUpdate(self, e):
497                 newVersion = version.checkForNewerVersion()
498                 if newVersion is not None:
499                         if wx.MessageBox('A new version of Cura is available, would you like to download?', 'New version available', wx.YES_NO | wx.ICON_INFORMATION) == wx.YES:
500                                 webbrowser.open(newVersion)
501                 else:
502                         wx.MessageBox('You are running the latest version of Cura!', 'Awesome!', wx.ICON_INFORMATION)
503
504         def OnClose(self, e):
505                 profile.saveGlobalProfile(profile.getDefaultProfilePath())
506
507                 # Save the window position, size & state from the preferences file
508                 profile.putPreference('window_maximized', self.IsMaximized())
509                 if not self.IsMaximized() and not self.IsIconized():
510                         (posx, posy) = self.GetPosition()
511                         profile.putPreference('window_pos_x', posx)
512                         profile.putPreference('window_pos_y', posy)
513                         (width, height) = self.GetSize()
514                         profile.putPreference('window_width', width)
515                         profile.putPreference('window_height', height)                  
516                         
517                         # Save normal sash position.  If in normal mode (!simple mode), get last position of sash before saving it...
518                         isSimple = profile.getPreference('startMode') == 'Simple'
519                         if not isSimple:
520                                 self.normalSashPos = self.splitter.GetSashPosition()
521                         profile.putPreference('window_normal_sash', self.normalSashPos)
522
523                 #HACK: Set the paint function of the glCanvas to nothing so it won't keep refreshing. Which keeps wxWidgets from quiting.
524                 self.preview3d.glCanvas.OnPaint = lambda e : e
525                 self.Destroy()
526
527         def OnQuit(self, e):
528                 self.Close()
529
530 class normalSettingsPanel(configBase.configPanelBase):
531         "Main user interface window"
532         def __init__(self, parent):
533                 super(normalSettingsPanel, self).__init__(parent)
534
535                 #Main tabs
536                 self.nb = wx.Notebook(self)
537                 self.SetSizer(wx.BoxSizer(wx.HORIZONTAL))
538                 self.GetSizer().Add(self.nb, 1, wx.EXPAND)
539
540                 (left, right, self.printPanel) = self.CreateDynamicConfigTab(self.nb, 'Basic')
541
542                 configBase.TitleRow(left, "Quality")
543                 c = configBase.SettingRow(left, "Layer height (mm)", 'layer_height', '0.2', 'Layer height in millimeters.\n0.2 is a good value for quick prints.\n0.1 gives high quality prints.')
544                 validators.validFloat(c, 0.0001)
545                 validators.warningAbove(c, lambda : (float(profile.getProfileSetting('nozzle_size')) * 80.0 / 100.0), "Thicker layers then %.2fmm (80%% nozzle size) usually give bad results and are not recommended.")
546                 c = configBase.SettingRow(left, "Wall thickness (mm)", 'wall_thickness', '0.8', 'Thickness of the walls.\nThis is used in combination with the nozzle size to define the number\nof perimeter lines and the thickness of those perimeter lines.')
547                 validators.validFloat(c, 0.0001)
548                 validators.wallThicknessValidator(c)
549 #               c = configBase.SettingRow(left, "Enable retraction", 'retraction_enable', False, 'Retract the filament when the nozzle is moving over a none-printed area. Details about the retraction can be configured in the advanced tab.')
550
551                 configBase.TitleRow(left, "Fill")
552                 c = configBase.SettingRow(left, "Bottom/Top thickness (mm)", 'solid_layer_thickness', '0.6', 'This controls the thickness of the bottom and top layers, the amount of solid layers put down is calculated by the layer thickness and this value.\nHaving this value a multiply of the layer thickness makes sense. And keep it near your wall thickness to make an evenly strong part.')
553                 validators.validFloat(c, 0.0)
554                 c = configBase.SettingRow(left, "Fill Density (%)", 'fill_density', '20', 'This controls how densily filled the insides of your print will be. For a solid part use 100%, for an empty part use 0%. A value around 20% is usually enough')
555                 validators.validFloat(c, 0.0, 100.0)
556
557                 configBase.TitleRow(right, "Speed && Temperature")
558                 c = configBase.SettingRow(right, "Print speed (mm/s)", 'print_speed', '50', 'Speed at which printing happens. A well adjusted Ultimaker can reach 150mm/s, but for good quality prints you want to print slower. Printing speed depends on a lot of factors. So you will be experimenting with optimal settings for this.')
559                 validators.validFloat(c, 1.0)
560                 validators.warningAbove(c, 150.0, "It is highly unlikely that your machine can achieve a printing speed above 150mm/s")
561                 validators.printSpeedValidator(c)
562
563                 #configBase.TitleRow(right, "Temperature")
564                 c = configBase.SettingRow(right, "Printing temperature", 'print_temperature', '0', 'Temperature used for printing. Set at 0 to pre-heat yourself')
565                 validators.validFloat(c, 0.0, 340.0)
566                 validators.warningAbove(c, 260.0, "Temperatures above 260C could damage your machine, be careful!")
567                 if profile.getPreference('has_heated_bed') == 'True':
568                         c = configBase.SettingRow(right, "Bed temperature", 'print_bed_temperature', '0', 'Temperature used for the heated printer bed. Set at 0 to pre-heat yourself')
569                         validators.validFloat(c, 0.0, 340.0)
570
571 #               configBase.TitleRow(right, "Support structure")
572 #               c = configBase.SettingRow(right, "Support type", 'support', ['None', 'Exterior Only', 'Everywhere'], 'Type of support structure build.\n"Exterior only" is the most commonly used support setting.\n\nNone does not do any support.\nExterior only only creates support where the support structure will touch the build platform.\nEverywhere creates support even on the insides of the model.')
573 #               c = configBase.SettingRow(right, "Add raft", 'enable_raft', False, 'A raft is a few layers of lines below the bottom of the object. It prevents warping. Full raft settings can be found in the expert settings.\nFor PLA this is usually not required. But if you print with ABS it is almost required.')
574 #               if int(profile.getPreference('extruder_amount')) > 1:
575 #                       c = configBase.SettingRow(right, "Support dual extrusion", 'support_dual_extrusion', False, 'Print the support material with the 2nd extruder in a dual extrusion setup. The primary extruder will be used for normal material, while the second extruder is used to print support material.')
576
577                 configBase.TitleRow(right, "Filament")
578                 c = configBase.SettingRow(right, "Diameter (mm)", 'filament_diameter', '2.89', 'Diameter of your filament, as accurately as possible.\nIf you cannot measure this value you will have to callibrate it, a higher number means less extrusion, a smaller number generates more extrusion.')
579                 validators.validFloat(c, 1.0)
580                 validators.warningAbove(c, 3.5, "Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm.")
581 #               c = configBase.SettingRow(right, "Packing Density", 'filament_density', '1.00', 'Packing density of your filament. This should be 1.00 for PLA and 0.85 for ABS')
582 #               validators.validFloat(c, 0.5, 1.5)
583
584                 self.SizeLabelWidths(left, right)
585                 
586                 (left, right, self.advancedPanel) = self.CreateDynamicConfigTab(self.nb, 'Advanced')
587                 
588                 configBase.TitleRow(left, "Machine size")
589                 c = configBase.SettingRow(left, "Nozzle size (mm)", 'nozzle_size', '0.4', 'The nozzle size is very important, this is used to calculate the line width of the infill, and used to calculate the amount of outside wall lines and thickness for the wall thickness you entered in the print settings.')
590                 validators.validFloat(c, 0.1, 10.0)
591
592                 configBase.TitleRow(left, "Skirt")
593                 c = configBase.SettingRow(left, "Line count", 'skirt_line_count', '1', 'The skirt is a line drawn around the object at the first layer. This helps to prime your extruder, and to see if the object fits on your platform.\nSetting this to 0 will disable the skirt. Multiple skirt lines can help priming your extruder better for small objects.')
594                 validators.validInt(c, 0, 10)
595                 c = configBase.SettingRow(left, "Start distance (mm)", 'skirt_gap', '6.0', 'The distance between the skirt and the first layer.\nThis is the minimal distance, multiple skirt lines will be put outwards from this distance.')
596                 validators.validFloat(c, 0.0)
597
598 #               configBase.TitleRow(left, "Retraction")
599 #               c = configBase.SettingRow(left, "Minimum travel (mm)", 'retraction_min_travel', '5.0', 'Minimum amount of travel needed for a retraction to happen at all. To make sure you do not get a lot of retractions in a small area')
600 #               validators.validFloat(c, 0.0)
601 #               c = configBase.SettingRow(left, "Speed (mm/s)", 'retraction_speed', '40.0', 'Speed at which the filament is retracted, a higher retraction speed works better. But a very high retraction speed can lead to filament grinding.')
602 #               validators.validFloat(c, 0.1)
603 #               c = configBase.SettingRow(left, "Distance (mm)", 'retraction_amount', '0.0', 'Amount of retraction, set at 0 for no retraction at all. A value of 2.0mm seems to generate good results.')
604 #               validators.validFloat(c, 0.0)
605 #               c = configBase.SettingRow(left, "Extra length on start (mm)", 'retraction_extra', '0.0', 'Extra extrusion amount when restarting after a retraction, to better "Prime" your extruder after retraction.')
606 #               validators.validFloat(c, 0.0)
607
608                 configBase.TitleRow(right, "Speed")
609                 c = configBase.SettingRow(right, "Travel speed (mm/s)", 'travel_speed', '150', 'Speed at which travel moves are done, a high quality build Ultimaker can reach speeds of 250mm/s. But some machines might miss steps then.')
610                 validators.validFloat(c, 1.0)
611                 validators.warningAbove(c, 300.0, "It is highly unlikely that your machine can achieve a travel speed above 300mm/s")
612 #               c = configBase.SettingRow(right, "Max Z speed (mm/s)", 'max_z_speed', '1.0', 'Speed at which Z moves are done. When you Z axis is properly lubercated you can increase this for less Z blob.')
613 #               validators.validFloat(c, 0.5)
614                 c = configBase.SettingRow(right, "Bottom layer speed (mm/s)", 'bottom_layer_speed', '25', 'Print speed for the bottom layer, you want to print the first layer slower so it sticks better to the printer bed.')
615                 validators.validFloat(c, 0.0)
616
617 #               configBase.TitleRow(right, "Cool")
618 #               c = configBase.SettingRow(right, "Minimal layer time (sec)", 'cool_min_layer_time', '10', 'Minimum time spend in a layer, gives the layer time to cool down before the next layer is put on top. If the layer will be placed down too fast the printer will slow down to make sure it has spend atleast this amount of seconds printing this layer.')
619 #               validators.validFloat(c, 0.0)
620 #               c = configBase.SettingRow(right, "Enable cooling fan", 'fan_enabled', True, 'Enable the cooling fan during the print. The extra cooling from the cooling fan is essensial during faster prints.')
621
622                 configBase.TitleRow(right, "Quality")
623                 c = configBase.SettingRow(right, "Initial layer thickness (mm)", 'bottom_thickness', '0.0', 'Layer thickness of the bottom layer. A thicker bottom layer makes sticking to the bed easier. Set to 0.0 to have the bottom layer thickness the same as the other layers.')
624                 validators.validFloat(c, 0.0)
625                 validators.warningAbove(c, lambda : (float(profile.getProfileSetting('nozzle_size')) * 3.0 / 4.0), "A bottom layer of more then %.2fmm (3/4 nozzle size) usually give bad results and is not recommended.")
626 #               c = configBase.SettingRow(right, "Cut off object bottom (mm)", 'object_sink', 0.05, '...')
627 #               validators.validFloat(c, 0.0)
628 #               configBase.settingNotify(c, lambda : self.GetParent().GetParent().GetParent().preview3d.Refresh())
629 #               c = configBase.SettingRow(right, "Duplicate outlines", 'enable_skin', False, 'Skin prints the outer lines of the prints twice, each time with half the thickness. This gives the illusion of a higher print quality.')
630
631                 self.SizeLabelWidths(left, right)
632
633                 #Plugin page
634                 self.pluginPanel = pluginPanel.pluginPanel(self.nb)
635                 if len(self.pluginPanel.pluginList) > 0:
636                         self.nb.AddPage(self.pluginPanel, "Plugins")
637                 else:
638                         self.pluginPanel.Show(False)
639
640                 #Alteration page
641                 self.alterationPanel = alterationPanel.alterationPanel(self.nb)
642                 self.nb.AddPage(self.alterationPanel, "Start/End-GCode")
643
644                 self.Bind(wx.EVT_SIZE, self.OnSize)
645
646         def SizeLabelWidths(self, left, right):
647                 leftWidth = self.getLabelColumnWidth(left)
648                 rightWidth = self.getLabelColumnWidth(right)
649                 maxWidth = max(leftWidth, rightWidth)
650                 self.setLabelColumnWidth(left, maxWidth)
651                 self.setLabelColumnWidth(right, maxWidth)
652
653         def OnSize(self, e):
654                 # Make the size of the Notebook control the same size as this control
655                 self.nb.SetSize(self.GetSize())
656                 
657                 # Propegate the OnSize() event (just in case)
658                 e.Skip()
659                 
660                 # Perform out resize magic
661                 self.UpdateSize(self.printPanel)
662                 self.UpdateSize(self.advancedPanel)
663         
664         def UpdateSize(self, configPanel):
665                 sizer = configPanel.GetSizer()
666                 
667                 # Pseudocde
668                 # if horizontal:
669                 #     if width(col1) < best_width(col1) || width(col2) < best_width(col2):
670                 #         switch to vertical
671                 # else:
672                 #     if width(col1) > (best_width(col1) + best_width(col1)):
673                 #         switch to horizontal
674                 #
675                                 
676                 col1 = configPanel.leftPanel
677                 colSize1 = col1.GetSize()
678                 colBestSize1 = col1.GetBestSize()
679                 col2 = configPanel.rightPanel
680                 colSize2 = col2.GetSize()
681                 colBestSize2 = col2.GetBestSize()
682
683                 orientation = sizer.GetOrientation()
684                 
685                 if orientation == wx.HORIZONTAL:
686                         if (colSize1[0] <= colBestSize1[0]) or (colSize2[0] <= colBestSize2[0]):
687                                 configPanel.Freeze()
688                                 sizer = wx.BoxSizer(wx.VERTICAL)
689                                 sizer.Add(configPanel.leftPanel, flag=wx.EXPAND)
690                                 sizer.Add(configPanel.rightPanel, flag=wx.EXPAND)
691                                 configPanel.SetSizer(sizer)
692                                 #sizer.Layout()
693                                 configPanel.Layout()
694                                 self.Layout()
695                                 configPanel.Thaw()
696                 else:
697                         if colSize1[0] > (colBestSize1[0] + colBestSize2[0]):
698                                 configPanel.Freeze()
699                                 sizer = wx.BoxSizer(wx.HORIZONTAL)
700                                 sizer.Add(configPanel.leftPanel, proportion=1, border=35, flag=wx.EXPAND)
701                                 sizer.Add(configPanel.rightPanel, proportion=1, flag=wx.EXPAND)
702                                 configPanel.SetSizer(sizer)
703                                 #sizer.Layout()
704                                 configPanel.Layout()
705                                 self.Layout()
706                                 configPanel.Thaw()
707                                 
708         def updateProfileToControls(self):
709                 super(normalSettingsPanel, self).updateProfileToControls()
710                 self.alterationPanel.updateProfileToControls()
711                 self.pluginPanel.updateProfileToControls()