chiark / gitweb /
Turn the preferences window into a dialog, which fixes the window not showing up...
[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 - ' + 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                 try:
211                         if profile.getPreference('window_maximized') == 'True':
212                                 self.Maximize(True)
213                         else:
214                                 posx = int(profile.getPreference('window_pos_x'))
215                                 posy = int(profile.getPreference('window_pos_y'))
216                                 width = int(profile.getPreference('window_width'))
217                                 height = int(profile.getPreference('window_height'))
218                                 if posx > 0 or posy > 0:
219                                         self.SetPosition((posx,posy))
220                                 if width > 0 and height > 0:
221                                         self.SetSize((width,height))
222                                 
223                         self.normalSashPos = int(profile.getPreference('window_normal_sash'))
224                 except:
225                         self.normalSashPos = 0
226                         self.Maximize(True)
227                 if self.normalSashPos < self.normalSettingsPanel.printPanel.GetBestSize()[0] + 5:
228                         self.normalSashPos = self.normalSettingsPanel.printPanel.GetBestSize()[0] + 5
229
230                 self.splitter.SplitVertically(self.leftPane, self.rightPane, self.normalSashPos)
231
232                 if wx.Display.GetFromPoint(self.GetPosition()) < 0:
233                         self.Centre()
234                 if wx.Display.GetFromPoint((self.GetPositionTuple()[0] + self.GetSizeTuple()[1], self.GetPositionTuple()[1] + self.GetSizeTuple()[1])) < 0:
235                         self.Centre()
236                 if wx.Display.GetFromPoint(self.GetPosition()) < 0:
237                         self.SetSize((800,600))
238                         self.Centre()
239
240                 self.updateSliceMode()
241
242                 self.Show(True)
243
244         def updateSliceMode(self):
245                 isSimple = profile.getPreference('startMode') == 'Simple'
246
247                 self.normalSettingsPanel.Show(not isSimple)
248                 self.simpleSettingsPanel.Show(isSimple)
249                 self.leftPane.Layout()
250
251                 for i in self.normalModeOnlyItems:
252                         i.Enable(not isSimple)
253                 self.switchToQuickprintMenuItem.Enable(not isSimple)
254                 self.switchToNormalMenuItem.Enable(isSimple)
255
256                 # Set splitter sash position & size
257                 if isSimple:
258                         # Save normal mode sash
259                         self.normalSashPos = self.splitter.GetSashPosition()
260                         
261                         # Change location of sash to width of quick mode pane 
262                         (width, height) = self.simpleSettingsPanel.GetSizer().GetSize() 
263                         self.splitter.SetSashPosition(width, True)
264                         
265                         # Disable sash
266                         self.splitter.SetSashSize(0)
267                 else:
268                         self.splitter.SetSashPosition(self.normalSashPos, True)
269                         # Enabled sash
270                         self.splitter.SetSashSize(4)
271                                                                 
272         def OnPreferences(self, e):
273                 prefDialog = preferencesDialog.preferencesDialog(self)
274                 prefDialog.Centre()
275                 prefDialog.Show()
276
277         def _showOpenDialog(self, title, wildcard = meshLoader.wildcardFilter()):
278                 dlg=wx.FileDialog(self, title, os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
279                 dlg.SetWildcard(wildcard)
280                 if dlg.ShowModal() == wx.ID_OK:
281                         filename = dlg.GetPath()
282                         dlg.Destroy()
283                         if not(os.path.exists(filename)):
284                                 return False
285                         profile.putPreference('lastFile', filename)
286                         return filename
287                 dlg.Destroy()
288                 return False
289
290         def _showModelLoadDialog(self, amount):
291                 filelist = []
292                 for i in xrange(0, amount):
293                         filelist.append(self._showOpenDialog("Open file to print"))
294                         if filelist[-1] == False:
295                                 return
296                 self._loadModels(filelist)
297
298         def _loadModels(self, filelist):
299                 self.filelist = filelist
300                 self.SetTitle('Cura - %s - %s' % (version.getVersion(), filelist[-1]))
301                 profile.putPreference('lastFile', ';'.join(self.filelist))
302                 self.preview3d.loadModelFiles(self.filelist, True)
303                 self.preview3d.setViewMode("Normal")
304                 
305                 # Update the Model MRU
306                 for idx in xrange(0, len(self.filelist)):
307                         self.addToModelMRU(self.filelist[idx])
308
309         def OnDropFiles(self, files):
310                 profile.putProfileSetting('model_matrix', '1,0,0,0,1,0,0,0,1')
311                 profile.setPluginConfig([])
312                 self.updateProfileToControls()
313                 self._loadModels(files)
314
315         def OnLoadModel(self, e):
316                 self._showModelLoadDialog(1)
317
318         def OnLoadModel2(self, e):
319                 self._showModelLoadDialog(2)
320
321         def OnLoadModel3(self, e):
322                 self._showModelLoadDialog(3)
323
324         def OnLoadModel4(self, e):
325                 self._showModelLoadDialog(4)
326
327         def OnSlice(self, e):
328                 if len(self.filelist) < 1:
329                         wx.MessageBox('You need to load a file before you can prepare it.', 'Print error', wx.OK | wx.ICON_INFORMATION)
330                         return
331                 isSimple = profile.getPreference('startMode') == 'Simple'
332                 if isSimple:
333                         #save the current profile so we can put it back latter
334                         oldProfile = profile.getGlobalProfileString()
335                         self.simpleSettingsPanel.setupSlice()
336                 #Create a progress panel and add it to the window. The progress panel will start the Skein operation.
337                 spp = sliceProgressPanel.sliceProgressPanel(self, self, self.filelist)
338                 self.sizer.Add(spp, 0, flag=wx.EXPAND)
339                 self.sizer.Layout()
340                 newSize = self.GetSize()
341                 newSize.IncBy(0, spp.GetSize().GetHeight())
342                 if newSize.GetWidth() < wx.GetDisplaySize()[0]:
343                         self.SetSize(newSize)
344                 self.progressPanelList.append(spp)
345                 if isSimple:
346                         profile.loadGlobalProfileFromString(oldProfile)
347
348         def OnPrint(self, e):
349                 if len(self.filelist) < 1:
350                         wx.MessageBox('You need to load a file and prepare it before you can print.', 'Print error', wx.OK | wx.ICON_INFORMATION)
351                         return
352                 if not os.path.exists(sliceRun.getExportFilename(self.filelist[0])):
353                         wx.MessageBox('You need to prepare a print before you can run the actual print.', 'Print error', wx.OK | wx.ICON_INFORMATION)
354                         return
355                 printWindow.printFile(sliceRun.getExportFilename(self.filelist[0]))
356
357         def OnModelMRU(self, e):
358                 fileNum = e.GetId() - self.ID_MRU_MODEL1
359                 path = self.modelFileHistory.GetHistoryFile(fileNum)
360                 # Update Model MRU
361                 self.modelFileHistory.AddFileToHistory(path)  # move up the list
362                 self.config.SetPath("/ModelMRU")
363                 self.modelFileHistory.Save(self.config)
364                 self.config.Flush()
365                 # Load Model
366                 filelist = [ path ]
367                 self._loadModels(filelist)
368
369         def addToModelMRU(self, file):
370                 self.modelFileHistory.AddFileToHistory(file)
371                 self.config.SetPath("/ModelMRU")
372                 self.modelFileHistory.Save(self.config)
373                 self.config.Flush()
374         
375         def OnProfileMRU(self, e):
376                 fileNum = e.GetId() - self.ID_MRU_PROFILE1
377                 path = self.profileFileHistory.GetHistoryFile(fileNum)
378                 # Update Profile MRU
379                 self.profileFileHistory.AddFileToHistory(path)  # move up the list
380                 self.config.SetPath("/ProfileMRU")
381                 self.profileFileHistory.Save(self.config)
382                 self.config.Flush()
383                 # Load Profile  
384                 profile.loadGlobalProfile(path)
385                 self.updateProfileToControls()
386
387         def addToProfileMRU(self, file):
388                 self.profileFileHistory.AddFileToHistory(file)
389                 self.config.SetPath("/ProfileMRU")
390                 self.profileFileHistory.Save(self.config)
391                 self.config.Flush()                     
392
393         def removeSliceProgress(self, spp):
394                 self.progressPanelList.remove(spp)
395                 newSize = self.GetSize()
396                 newSize.IncBy(0, -spp.GetSize().GetHeight())
397                 if newSize.GetWidth() < wx.GetDisplaySize()[0]:
398                         self.SetSize(newSize)
399                 spp.Show(False)
400                 self.sizer.Detach(spp)
401                 self.sizer.Layout()
402
403         def updateProfileToControls(self):
404                 self.preview3d.updateProfileToControls()
405                 self.normalSettingsPanel.updateProfileToControls()
406                 self.simpleSettingsPanel.updateProfileToControls()
407
408         def OnLoadProfile(self, e):
409                 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)
410                 dlg.SetWildcard("ini files (*.ini)|*.ini")
411                 if dlg.ShowModal() == wx.ID_OK:
412                         profileFile = dlg.GetPath()
413                         profile.loadGlobalProfile(profileFile)
414                         self.updateProfileToControls()
415
416                         # Update the Profile MRU
417                         self.addToProfileMRU(profileFile)
418                 dlg.Destroy()
419
420         def OnLoadProfileFromGcode(self, e):
421                 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)
422                 dlg.SetWildcard("gcode files (*.gcode)|*.gcode;*.g")
423                 if dlg.ShowModal() == wx.ID_OK:
424                         gcodeFile = dlg.GetPath()
425                         f = open(gcodeFile, 'r')
426                         hasProfile = False
427                         for line in f:
428                                 if line.startswith(';CURA_PROFILE_STRING:'):
429                                         profile.loadGlobalProfileFromString(line[line.find(':')+1:].strip())
430                                         hasProfile = True
431                         if hasProfile:
432                                 self.updateProfileToControls()
433                         else:
434                                 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)
435                 dlg.Destroy()
436
437         def OnSaveProfile(self, e):
438                 dlg=wx.FileDialog(self, "Select profile file to save", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
439                 dlg.SetWildcard("ini files (*.ini)|*.ini")
440                 if dlg.ShowModal() == wx.ID_OK:
441                         profileFile = dlg.GetPath()
442                         profile.saveGlobalProfile(profileFile)
443                 dlg.Destroy()
444
445         def OnResetProfile(self, e):
446                 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)
447                 result = dlg.ShowModal() == wx.ID_YES
448                 dlg.Destroy()
449                 if result:
450                         profile.resetGlobalProfile()
451                         self.updateProfileToControls()
452
453         def OnBatchRun(self, e):
454                 br = batchRun.batchRunWindow(self)
455                 br.Centre()
456                 br.Show(True)
457
458         def OnSimpleSwitch(self, e):
459                 profile.putPreference('startMode', 'Simple')
460                 self.updateSliceMode()
461
462         def OnNormalSwitch(self, e):
463                 profile.putPreference('startMode', 'Normal')
464                 self.updateSliceMode()
465
466         def OnDefaultMarlinFirmware(self, e):
467                 firmwareInstall.InstallFirmware()
468
469         def OnCustomFirmware(self, e):
470                 if profile.getPreference('machine_type') == 'ultimaker':
471                         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)
472                 dlg=wx.FileDialog(self, "Open firmware to upload", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
473                 dlg.SetWildcard("HEX file (*.hex)|*.hex;*.HEX")
474                 if dlg.ShowModal() == wx.ID_OK:
475                         filename = dlg.GetPath()
476                         if not(os.path.exists(filename)):
477                                 return
478                         #For some reason my Ubuntu 10.10 crashes here.
479                         firmwareInstall.InstallFirmware(filename)
480
481         def OnFirstRunWizard(self, e):
482                 configWizard.configWizard()
483                 self.updateProfileToControls()
484
485         def OnBedLevelWizard(self, e):
486                 configWizard.bedLevelWizard()
487
488         def OnExpertOpen(self, e):
489                 ecw = expertConfig.expertConfigWindow()
490                 ecw.Centre()
491                 ecw.Show(True)
492
493         def OnProjectPlanner(self, e):
494                 pp = projectPlanner.projectPlanner()
495                 pp.Centre()
496                 pp.Show(True)
497
498         def OnMinecraftImport(self, e):
499                 mi = minecraftImport.minecraftImportWindow(self)
500                 mi.Centre()
501                 mi.Show(True)
502
503         def OnSVGSlicerOpen(self, e):
504                 svgSlicer = flatSlicerWindow.flatSlicerWindow()
505                 svgSlicer.Centre()
506                 svgSlicer.Show(True)
507
508         def OnCheckForUpdate(self, e):
509                 newVersion = version.checkForNewerVersion()
510                 if newVersion is not None:
511                         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:
512                                 webbrowser.open(newVersion)
513                 else:
514                         wx.MessageBox('You are running the latest version of Cura!', 'Awesome!', wx.ICON_INFORMATION)
515
516         def OnClose(self, e):
517                 profile.saveGlobalProfile(profile.getDefaultProfilePath())
518
519                 # Save the window position, size & state from the preferences file
520                 profile.putPreference('window_maximized', self.IsMaximized())
521                 if not self.IsMaximized() and not self.IsIconized():
522                         (posx, posy) = self.GetPosition()
523                         profile.putPreference('window_pos_x', posx)
524                         profile.putPreference('window_pos_y', posy)
525                         (width, height) = self.GetSize()
526                         profile.putPreference('window_width', width)
527                         profile.putPreference('window_height', height)                  
528                         
529                         # Save normal sash position.  If in normal mode (!simple mode), get last position of sash before saving it...
530                         isSimple = profile.getPreference('startMode') == 'Simple'
531                         if not isSimple:
532                                 self.normalSashPos = self.splitter.GetSashPosition()
533                         profile.putPreference('window_normal_sash', self.normalSashPos)
534
535                 #HACK: Set the paint function of the glCanvas to nothing so it won't keep refreshing. Which keeps wxWidgets from quiting.
536                 self.preview3d.glCanvas.OnPaint = lambda e : e
537                 self.Destroy()
538
539         def OnQuit(self, e):
540                 self.Close()
541
542 class normalSettingsPanel(configBase.configPanelBase):
543         "Main user interface window"
544         def __init__(self, parent):
545                 super(normalSettingsPanel, self).__init__(parent)
546
547                 #Main tabs
548                 self.nb = wx.Notebook(self)
549                 self.SetSizer(wx.BoxSizer(wx.HORIZONTAL))
550                 self.GetSizer().Add(self.nb, 1, wx.EXPAND)
551
552                 (left, right, self.printPanel) = self.CreateDynamicConfigTab(self.nb, 'Basic')
553
554                 configBase.TitleRow(left, "Quality")
555                 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.')
556                 validators.validFloat(c, 0.0001)
557                 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.")
558                 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.')
559                 validators.validFloat(c, 0.0001)
560                 validators.wallThicknessValidator(c)
561                 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.')
562
563                 configBase.TitleRow(left, "Fill")
564                 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.')
565                 validators.validFloat(c, 0.0)
566                 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')
567                 validators.validFloat(c, 0.0, 100.0)
568
569                 configBase.TitleRow(right, "Speed && Temperature")
570                 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.')
571                 validators.validFloat(c, 1.0)
572                 validators.warningAbove(c, 150.0, "It is highly unlikely that your machine can achieve a printing speed above 150mm/s")
573                 validators.printSpeedValidator(c)
574
575                 #configBase.TitleRow(right, "Temperature")
576                 c = configBase.SettingRow(right, "Printing temperature", 'print_temperature', '0', 'Temperature used for printing. Set at 0 to pre-heat yourself')
577                 validators.validFloat(c, 0.0, 340.0)
578                 validators.warningAbove(c, 260.0, "Temperatures above 260C could damage your machine, be careful!")
579                 if int(profile.getPreference('extruder_amount')) > 1:
580                         c = configBase.SettingRow(right, "2nd nozzle temperature", 'print_temperature2', '0', 'Temperature used for printing with the 2nd nozzle. Set at 0 to use the same temperature as for nozzle 1')
581                         validators.validFloat(c, 0.0, 340.0)
582                         validators.warningAbove(c, 260.0, "Temperatures above 260C could damage your machine, be careful!")
583                 if int(profile.getPreference('extruder_amount')) > 2:
584                         c = configBase.SettingRow(right, "3th nozzle temperature", 'print_temperature3', '0', 'Temperature used for printing with the 3th nozzle. Set at 0 to use the same temperature as for nozzle 1')
585                         validators.validFloat(c, 0.0, 340.0)
586                         validators.warningAbove(c, 260.0, "Temperatures above 260C could damage your machine, be careful!")
587                 if int(profile.getPreference('extruder_amount')) > 3:
588                         c = configBase.SettingRow(right, "4th nozzle temperature", 'print_temperature4', '0', 'Temperature used for printing with the 4th nozzle. Set at 0 to use the same temperature as for nozzle 1')
589                         validators.validFloat(c, 0.0, 340.0)
590                         validators.warningAbove(c, 260.0, "Temperatures above 260C could damage your machine, be careful!")
591                 if profile.getPreference('has_heated_bed') == 'True':
592                         c = configBase.SettingRow(right, "Bed temperature", 'print_bed_temperature', '0', 'Temperature used for the heated printer bed. Set at 0 to pre-heat yourself')
593                         validators.validFloat(c, 0.0, 340.0)
594
595                 configBase.TitleRow(right, "Support structure")
596                 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.')
597                 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.')
598                 if int(profile.getPreference('extruder_amount')) > 1:
599                         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.')
600
601                 configBase.TitleRow(right, "Filament")
602                 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 calibrate it, a higher number means less extrusion, a smaller number generates more extrusion.')
603                 validators.validFloat(c, 1.0)
604                 validators.warningAbove(c, 3.5, "Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm.")
605                 if int(profile.getPreference('extruder_amount')) > 1:
606                         c = configBase.SettingRow(right, "Diameter (mm)", 'filament_diameter2', '2.89', 'Diameter of your filament for the 2nd nozzle, as accurately as possible.\nIf you cannot measure this value you will have to calibrate it, a higher number means less extrusion, a smaller number generates more extrusion. Use 0 to use the same diameter as for nozzle 1.')
607                         validators.validFloat(c, 0.0)
608                         validators.warningAbove(c, 3.5, "Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm.")
609                 if int(profile.getPreference('extruder_amount')) > 2:
610                         c = configBase.SettingRow(right, "Diameter (mm)", 'filament_diameter3', '2.89', 'Diameter of your filament for the 3th nozzle, as accurately as possible.\nIf you cannot measure this value you will have to calibrate it, a higher number means less extrusion, a smaller number generates more extrusion. Use 0 to use the same diameter as for nozzle 1.')
611                         validators.validFloat(c, 0.0)
612                         validators.warningAbove(c, 3.5, "Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm.")
613                 if int(profile.getPreference('extruder_amount')) > 3:
614                         c = configBase.SettingRow(right, "Diameter (mm)", 'filament_diameter4', '2.89', 'Diameter of your filament for the 4th nozzle, as accurately as possible.\nIf you cannot measure this value you will have to calibrate it, a higher number means less extrusion, a smaller number generates more extrusion. Use 0 to use the same diameter as for nozzle 1.')
615                         validators.validFloat(c, 0.0)
616                         validators.warningAbove(c, 3.5, "Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm.")
617                 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')
618                 validators.validFloat(c, 0.5, 1.5)
619
620                 self.SizeLabelWidths(left, right)
621                 
622                 (left, right, self.advancedPanel) = self.CreateDynamicConfigTab(self.nb, 'Advanced')
623                 
624                 configBase.TitleRow(left, "Machine size")
625                 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.')
626                 validators.validFloat(c, 0.1, 10.0)
627
628                 configBase.TitleRow(left, "Skirt")
629                 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.')
630                 validators.validInt(c, 0, 10)
631                 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.')
632                 validators.validFloat(c, 0.0)
633
634                 configBase.TitleRow(left, "Retraction")
635                 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')
636                 validators.validFloat(c, 0.0)
637                 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.')
638                 validators.validFloat(c, 0.1)
639                 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.')
640                 validators.validFloat(c, 0.0)
641                 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.')
642                 validators.validFloat(c, 0.0)
643
644                 configBase.TitleRow(right, "Speed")
645                 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.')
646                 validators.validFloat(c, 1.0)
647                 validators.warningAbove(c, 300.0, "It is highly unlikely that your machine can achieve a travel speed above 300mm/s")
648                 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.')
649                 validators.validFloat(c, 0.5)
650                 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.')
651                 validators.validFloat(c, 0.0)
652
653                 configBase.TitleRow(right, "Cool")
654                 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.')
655                 validators.validFloat(c, 0.0)
656                 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.')
657
658                 configBase.TitleRow(right, "Quality")
659                 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.')
660                 validators.validFloat(c, 0.0)
661                 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.")
662                 c = configBase.SettingRow(right, "Cut off object bottom (mm)", 'object_sink', '0.00', 'Sinks the object into the platform, this can be used for objects that do not have a flat bottom and thus create a too small first layer.')
663                 validators.validFloat(c, 0.0)
664                 configBase.settingNotify(c, lambda : self.GetParent().GetParent().GetParent().preview3d.Refresh())
665                 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.')
666
667                 self.SizeLabelWidths(left, right)
668
669                 #Plugin page
670                 self.pluginPanel = pluginPanel.pluginPanel(self.nb)
671                 if len(self.pluginPanel.pluginList) > 0:
672                         self.nb.AddPage(self.pluginPanel, "Plugins")
673                 else:
674                         self.pluginPanel.Show(False)
675
676                 #Alteration page
677                 self.alterationPanel = alterationPanel.alterationPanel(self.nb)
678                 self.nb.AddPage(self.alterationPanel, "Start/End-GCode")
679
680                 self.Bind(wx.EVT_SIZE, self.OnSize)
681
682                 self.nb.SetSize(self.GetSize())
683                 self.UpdateSize(self.printPanel)
684                 self.UpdateSize(self.advancedPanel)
685
686         def SizeLabelWidths(self, left, right):
687                 leftWidth = self.getLabelColumnWidth(left)
688                 rightWidth = self.getLabelColumnWidth(right)
689                 maxWidth = max(leftWidth, rightWidth)
690                 self.setLabelColumnWidth(left, maxWidth)
691                 self.setLabelColumnWidth(right, maxWidth)
692
693         def OnSize(self, e):
694                 # Make the size of the Notebook control the same size as this control
695                 self.nb.SetSize(self.GetSize())
696                 
697                 # Propegate the OnSize() event (just in case)
698                 e.Skip()
699                 
700                 # Perform out resize magic
701                 self.UpdateSize(self.printPanel)
702                 self.UpdateSize(self.advancedPanel)
703         
704         def UpdateSize(self, configPanel):
705                 sizer = configPanel.GetSizer()
706                 
707                 # Pseudocde
708                 # if horizontal:
709                 #     if width(col1) < best_width(col1) || width(col2) < best_width(col2):
710                 #         switch to vertical
711                 # else:
712                 #     if width(col1) > (best_width(col1) + best_width(col1)):
713                 #         switch to horizontal
714                 #
715                                 
716                 col1 = configPanel.leftPanel
717                 colSize1 = col1.GetSize()
718                 colBestSize1 = col1.GetBestSize()
719                 col2 = configPanel.rightPanel
720                 colSize2 = col2.GetSize()
721                 colBestSize2 = col2.GetBestSize()
722
723                 orientation = sizer.GetOrientation()
724                 
725                 if orientation == wx.HORIZONTAL:
726                         if (colSize1[0] <= colBestSize1[0]) or (colSize2[0] <= colBestSize2[0]):
727                                 configPanel.Freeze()
728                                 sizer = wx.BoxSizer(wx.VERTICAL)
729                                 sizer.Add(configPanel.leftPanel, flag=wx.EXPAND)
730                                 sizer.Add(configPanel.rightPanel, flag=wx.EXPAND)
731                                 configPanel.SetSizer(sizer)
732                                 #sizer.Layout()
733                                 configPanel.Layout()
734                                 self.Layout()
735                                 configPanel.Thaw()
736                 else:
737                         if max(colSize1[0], colSize2[0]) > (colBestSize1[0] + colBestSize2[0]):
738                                 configPanel.Freeze()
739                                 sizer = wx.BoxSizer(wx.HORIZONTAL)
740                                 sizer.Add(configPanel.leftPanel, proportion=1, border=35, flag=wx.EXPAND)
741                                 sizer.Add(configPanel.rightPanel, proportion=1, flag=wx.EXPAND)
742                                 configPanel.SetSizer(sizer)
743                                 #sizer.Layout()
744                                 configPanel.Layout()
745                                 self.Layout()
746                                 configPanel.Thaw()
747
748         def updateProfileToControls(self):
749                 super(normalSettingsPanel, self).updateProfileToControls()
750                 self.alterationPanel.updateProfileToControls()
751                 self.pluginPanel.updateProfileToControls()