chiark / gitweb /
16c215c898f63c55e24e8c714b6c8be0ec23ad77
[cura.git] / Cura / gui / mainWindow.py
1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
2
3 import wx
4 import os
5 import webbrowser
6 import sys
7
8
9 from Cura.gui import configBase
10 from Cura.gui import expertConfig
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 simpleMode
17 from Cura.gui import sceneView
18 from Cura.gui import aboutWindow
19 from Cura.gui.util import dropTarget
20 #from Cura.gui.tools import batchRun
21 from Cura.gui.tools import pidDebugger
22 from Cura.gui.tools import minecraftImport
23 from Cura.util import profile
24 from Cura.util import version
25 import platform
26 from Cura.util import meshLoader
27
28 try:
29         #MacOS release currently lacks some wx components, like the Publisher.
30         from wx.lib.pubsub import Publisher
31 except:
32         Publisher = None
33
34 class mainWindow(wx.Frame):
35         def __init__(self):
36                 super(mainWindow, self).__init__(None, title=_('Cura - ') + version.getVersion())
37
38                 wx.EVT_CLOSE(self, self.OnClose)
39
40                 # allow dropping any file, restrict later
41                 self.SetDropTarget(dropTarget.FileDropTarget(self.OnDropFiles))
42
43                 # TODO: wxWidgets 2.9.4 has a bug when NSView does not register for dragged types when wx drop target is set. It was fixed in 2.9.5
44                 if sys.platform.startswith('darwin'):
45                         try:
46                                 import objc
47                                 nswindow = objc.objc_object(c_void_p=self.MacGetTopLevelWindowRef())
48                                 view = nswindow.contentView()
49                                 view.registerForDraggedTypes_([u'NSFilenamesPboardType'])
50                         except:
51                                 pass
52
53                 self.normalModeOnlyItems = []
54
55                 mruFile = os.path.join(profile.getBasePath(), 'mru_filelist.ini')
56                 self.config = wx.FileConfig(appName="Cura",
57                                                 localFilename=mruFile,
58                                                 style=wx.CONFIG_USE_LOCAL_FILE)
59
60                 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)]
61                 self.modelFileHistory = wx.FileHistory(10, self.ID_MRU_MODEL1)
62                 self.config.SetPath("/ModelMRU")
63                 self.modelFileHistory.Load(self.config)
64
65                 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)]
66                 self.profileFileHistory = wx.FileHistory(10, self.ID_MRU_PROFILE1)
67                 self.config.SetPath("/ProfileMRU")
68                 self.profileFileHistory.Load(self.config)
69
70                 self.menubar = wx.MenuBar()
71                 self.fileMenu = wx.Menu()
72                 i = self.fileMenu.Append(-1, _("Load model file...\tCTRL+L"))
73                 self.Bind(wx.EVT_MENU, lambda e: self.scene.showLoadModel(), i)
74                 i = self.fileMenu.Append(-1, _("Save model...\tCTRL+S"))
75                 self.Bind(wx.EVT_MENU, lambda e: self.scene.showSaveModel(), i)
76                 i = self.fileMenu.Append(-1, _("Reload platform\tF5"))
77                 self.Bind(wx.EVT_MENU, lambda e: self.scene.reloadScene(e), i)
78                 i = self.fileMenu.Append(-1, _("Clear platform"))
79                 self.Bind(wx.EVT_MENU, lambda e: self.scene.OnDeleteAll(e), i)
80
81                 self.fileMenu.AppendSeparator()
82                 i = self.fileMenu.Append(-1, _("Print...\tCTRL+P"))
83                 self.Bind(wx.EVT_MENU, lambda e: self.scene.OnPrintButton(1), i)
84                 i = self.fileMenu.Append(-1, _("Save GCode..."))
85                 self.Bind(wx.EVT_MENU, lambda e: self.scene.showSaveGCode(), i)
86                 i = self.fileMenu.Append(-1, _("Show slice engine log..."))
87                 self.Bind(wx.EVT_MENU, lambda e: self.scene._showEngineLog(), i)
88
89                 self.fileMenu.AppendSeparator()
90                 i = self.fileMenu.Append(-1, _("Open Profile..."))
91                 self.normalModeOnlyItems.append(i)
92                 self.Bind(wx.EVT_MENU, self.OnLoadProfile, i)
93                 i = self.fileMenu.Append(-1, _("Save Profile..."))
94                 self.normalModeOnlyItems.append(i)
95                 self.Bind(wx.EVT_MENU, self.OnSaveProfile, i)
96                 if version.isDevVersion():
97                         i = self.fileMenu.Append(-1, "Save difference from default...")
98                         self.normalModeOnlyItems.append(i)
99                         self.Bind(wx.EVT_MENU, self.OnSaveDifferences, i)
100                 i = self.fileMenu.Append(-1, _("Load Profile from GCode..."))
101                 self.normalModeOnlyItems.append(i)
102                 self.Bind(wx.EVT_MENU, self.OnLoadProfileFromGcode, i)
103                 self.fileMenu.AppendSeparator()
104                 i = self.fileMenu.Append(-1, _("Reset Profile to default"))
105                 self.normalModeOnlyItems.append(i)
106                 self.Bind(wx.EVT_MENU, self.OnResetProfile, i)
107
108                 self.fileMenu.AppendSeparator()
109                 i = self.fileMenu.Append(-1, _("Preferences...\tCTRL+,"))
110                 self.Bind(wx.EVT_MENU, self.OnPreferences, i)
111                 i = self.fileMenu.Append(-1, _("Machine settings..."))
112                 self.Bind(wx.EVT_MENU, self.OnMachineSettings, i)
113                 self.fileMenu.AppendSeparator()
114
115                 # Model MRU list
116                 modelHistoryMenu = wx.Menu()
117                 self.fileMenu.AppendMenu(wx.NewId(), '&' + _("Recent Model Files"), modelHistoryMenu)
118                 self.modelFileHistory.UseMenu(modelHistoryMenu)
119                 self.modelFileHistory.AddFilesToMenu()
120                 self.Bind(wx.EVT_MENU_RANGE, self.OnModelMRU, id=self.ID_MRU_MODEL1, id2=self.ID_MRU_MODEL10)
121
122                 # Profle MRU list
123                 profileHistoryMenu = wx.Menu()
124                 self.fileMenu.AppendMenu(wx.NewId(), _("Recent Profile Files"), profileHistoryMenu)
125                 self.profileFileHistory.UseMenu(profileHistoryMenu)
126                 self.profileFileHistory.AddFilesToMenu()
127                 self.Bind(wx.EVT_MENU_RANGE, self.OnProfileMRU, id=self.ID_MRU_PROFILE1, id2=self.ID_MRU_PROFILE10)
128
129                 self.fileMenu.AppendSeparator()
130                 i = self.fileMenu.Append(wx.ID_EXIT, _("Quit"))
131                 self.Bind(wx.EVT_MENU, self.OnQuit, i)
132                 self.menubar.Append(self.fileMenu, '&' + _("File"))
133
134                 toolsMenu = wx.Menu()
135                 #i = toolsMenu.Append(-1, 'Batch run...')
136                 #self.Bind(wx.EVT_MENU, self.OnBatchRun, i)
137                 #self.normalModeOnlyItems.append(i)
138
139                 if minecraftImport.hasMinecraft():
140                         i = toolsMenu.Append(-1, _("Minecraft map import..."))
141                         self.Bind(wx.EVT_MENU, self.OnMinecraftImport, i)
142
143                 if version.isDevVersion():
144                         i = toolsMenu.Append(-1, _("PID Debugger..."))
145                         self.Bind(wx.EVT_MENU, self.OnPIDDebugger, i)
146                         i = toolsMenu.Append(-1, _("Auto Firmware Update..."))
147                         self.Bind(wx.EVT_MENU, self.OnAutoFirmwareUpdate, i)
148
149                 i = toolsMenu.Append(-1, _("Copy profile to clipboard"))
150                 self.Bind(wx.EVT_MENU, self.onCopyProfileClipboard,i)
151
152                 toolsMenu.AppendSeparator()
153                 self.allAtOnceItem = toolsMenu.Append(-1, _("Print all at once"), kind=wx.ITEM_RADIO)
154                 self.Bind(wx.EVT_MENU, self.onOneAtATimeSwitch, self.allAtOnceItem)
155                 self.oneAtATime = toolsMenu.Append(-1, _("Print one at a time"), kind=wx.ITEM_RADIO)
156                 self.Bind(wx.EVT_MENU, self.onOneAtATimeSwitch, self.oneAtATime)
157                 if profile.getPreference('oneAtATime') == 'True':
158                         self.oneAtATime.Check(True)
159                 else:
160                         self.allAtOnceItem.Check(True)
161
162                 self.menubar.Append(toolsMenu, _("Tools"))
163
164                 #Machine menu for machine configuration/tooling
165                 self.machineMenu = wx.Menu()
166                 self.updateMachineMenu()
167
168                 self.menubar.Append(self.machineMenu, _("Machine"))
169
170                 expertMenu = wx.Menu()
171                 i = expertMenu.Append(-1, _("Switch to quickprint..."), kind=wx.ITEM_RADIO)
172                 self.switchToQuickprintMenuItem = i
173                 self.Bind(wx.EVT_MENU, self.OnSimpleSwitch, i)
174
175                 i = expertMenu.Append(-1, _("Switch to full settings..."), kind=wx.ITEM_RADIO)
176                 self.switchToNormalMenuItem = i
177                 self.Bind(wx.EVT_MENU, self.OnNormalSwitch, i)
178                 expertMenu.AppendSeparator()
179
180                 i = expertMenu.Append(-1, _("Open expert settings...\tCTRL+E"))
181                 self.normalModeOnlyItems.append(i)
182                 self.Bind(wx.EVT_MENU, self.OnExpertOpen, i)
183                 expertMenu.AppendSeparator()
184                 self.bedLevelWizardMenuItem = expertMenu.Append(-1, _("Run bed leveling wizard..."))
185                 self.Bind(wx.EVT_MENU, self.OnBedLevelWizard, self.bedLevelWizardMenuItem)
186                 self.headOffsetWizardMenuItem = expertMenu.Append(-1, _("Run head offset wizard..."))
187                 self.Bind(wx.EVT_MENU, self.OnHeadOffsetWizard, self.headOffsetWizardMenuItem)
188
189                 self.menubar.Append(expertMenu, _("Expert"))
190
191                 helpMenu = wx.Menu()
192                 i = helpMenu.Append(-1, _("Online documentation..."))
193                 self.Bind(wx.EVT_MENU, lambda e: webbrowser.open('http://lulzbot.com/cura'), i)
194                 i = helpMenu.Append(-1, _("Report a problem..."))
195                 self.Bind(wx.EVT_MENU, lambda e: webbrowser.open('https://github.com/alephobjects/Cura/issues'), i)
196                 #i = helpMenu.Append(-1, _("Check for update..."))
197                 #self.Bind(wx.EVT_MENU, self.OnCheckForUpdate, i)
198                 #i = helpMenu.Append(-1, _("Open YouMagine website..."))
199                 #self.Bind(wx.EVT_MENU, lambda e: webbrowser.open('https://www.youmagine.com/'), i)
200                 i = helpMenu.Append(-1, _("About Cura..."))
201                 self.Bind(wx.EVT_MENU, self.OnAbout, i)
202                 self.menubar.Append(helpMenu, _("Help"))
203                 self.SetMenuBar(self.menubar)
204
205                 self.splitter = wx.SplitterWindow(self, style = wx.SP_3D | wx.SP_LIVE_UPDATE)
206                 self.leftPane = wx.Panel(self.splitter, style=wx.BORDER_NONE)
207                 self.rightPane = wx.Panel(self.splitter, style=wx.BORDER_NONE)
208                 self.splitter.Bind(wx.EVT_SPLITTER_DCLICK, lambda evt: evt.Veto())
209
210                 #Preview window
211                 self.scene = sceneView.SceneView(self.rightPane)
212
213                 ##Gui components##
214                 self.simpleSettingsPanel = simpleMode.simpleModePanel(self.leftPane, self.scene.sceneUpdated)
215                 self.normalSettingsPanel = normalSettingsPanel(self.leftPane, self.scene.sceneUpdated)
216
217                 self.leftSizer = wx.BoxSizer(wx.VERTICAL)
218                 self.leftSizer.Add(self.simpleSettingsPanel, 1)
219                 self.leftSizer.Add(self.normalSettingsPanel, 1, wx.EXPAND)
220                 self.leftPane.SetSizer(self.leftSizer)
221
222                 #Main sizer, to position the preview window, buttons and tab control
223                 sizer = wx.BoxSizer()
224                 self.rightPane.SetSizer(sizer)
225                 sizer.Add(self.scene, 1, flag=wx.EXPAND)
226
227                 # Main window sizer
228                 sizer = wx.BoxSizer(wx.VERTICAL)
229                 self.SetSizer(sizer)
230                 sizer.Add(self.splitter, 1, wx.EXPAND)
231                 sizer.Layout()
232                 self.sizer = sizer
233
234                 self.updateProfileToAllControls()
235
236                 self.SetBackgroundColour(self.normalSettingsPanel.GetBackgroundColour())
237
238                 self.simpleSettingsPanel.Show(False)
239                 self.normalSettingsPanel.Show(False)
240
241                 # Set default window size & position
242                 self.SetSize((wx.Display().GetClientArea().GetWidth()/2,wx.Display().GetClientArea().GetHeight()/2))
243                 self.Centre()
244
245                 #Timer set; used to check if profile is on the clipboard
246                 self.timer = wx.Timer(self)
247                 self.Bind(wx.EVT_TIMER, self.onTimer)
248                 self.timer.Start(1000)
249                 self.lastTriedClipboard = profile.getProfileString()
250
251                 # Restore the window position, size & state from the preferences file
252                 try:
253                         if profile.getPreference('window_maximized') == 'True':
254                                 self.Maximize(True)
255                         else:
256                                 posx = int(profile.getPreference('window_pos_x'))
257                                 posy = int(profile.getPreference('window_pos_y'))
258                                 width = int(profile.getPreference('window_width'))
259                                 height = int(profile.getPreference('window_height'))
260                                 if posx > 0 or posy > 0:
261                                         self.SetPosition((posx,posy))
262                                 if width > 0 and height > 0:
263                                         self.SetSize((width,height))
264
265                         self.normalSashPos = int(profile.getPreference('window_normal_sash'))
266                 except:
267                         self.normalSashPos = 0
268                         self.Maximize(True)
269                 if self.normalSashPos < self.normalSettingsPanel.printPanel.GetBestSize()[0] + 5:
270                         self.normalSashPos = self.normalSettingsPanel.printPanel.GetBestSize()[0] + 5
271
272                 self.splitter.SplitVertically(self.leftPane, self.rightPane, self.normalSashPos)
273
274                 if wx.Display.GetFromPoint(self.GetPosition()) < 0:
275                         self.Centre()
276                 if wx.Display.GetFromPoint((self.GetPositionTuple()[0] + self.GetSizeTuple()[1], self.GetPositionTuple()[1] + self.GetSizeTuple()[1])) < 0:
277                         self.Centre()
278                 if wx.Display.GetFromPoint(self.GetPosition()) < 0:
279                         self.SetSize((800,600))
280                         self.Centre()
281
282                 self.updateSliceMode()
283                 self.scene.SetFocus()
284                 self.dialogframe = None
285                 if Publisher is not None:
286                         Publisher().subscribe(self.onPluginUpdate, "pluginupdate")
287
288         def onPluginUpdate(self,msg): #receives commands from the plugin thread
289                 cmd = str(msg.data).split(";")
290                 if cmd[0] == "OpenPluginProgressWindow":
291                         if len(cmd)==1: #no titel received
292                                 cmd.append("Plugin")
293                         if len(cmd)<3: #no message text received
294                                 cmd.append("Plugin is executed...")
295                         dialogwidth = 300
296                         dialogheight = 80
297                         self.dialogframe = wx.Frame(self, -1, cmd[1],pos = ((wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X)-dialogwidth)/2,(wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)-dialogheight)/2), size=(dialogwidth,dialogheight), style = wx.STAY_ON_TOP)
298                         self.dialogpanel = wx.Panel(self.dialogframe, -1, pos = (0,0), size = (dialogwidth,dialogheight))
299                         self.dlgtext = wx.StaticText(self.dialogpanel, label = cmd[2], pos = (10,10), size = (280,40))
300                         self.dlgbar = wx.Gauge(self.dialogpanel,-1, 100, pos = (10,50), size = (280,20), style = wx.GA_HORIZONTAL)
301                         self.dialogframe.Show()
302
303                 elif cmd[0] == "Progress":
304                         number = int(cmd[1])
305                         if number <= 100 and self.dialogframe is not None:
306                                 self.dlgbar.SetValue(number)
307                         else:
308                                 self.dlgbar.SetValue(100)
309                 elif cmd[0] == "ClosePluginProgressWindow":
310                         self.dialogframe.Destroy()
311                         self.dialogframe=None
312                 else:
313                         print "Unknown Plugin update received: " + cmd[0]
314
315         def onTimer(self, e):
316                 #Check if there is something in the clipboard
317                 profileString = ""
318                 try:
319                         if not wx.TheClipboard.IsOpened():
320                                 if not wx.TheClipboard.Open():
321                                         return
322                                 do = wx.TextDataObject()
323                                 if wx.TheClipboard.GetData(do):
324                                         profileString = do.GetText()
325                                 wx.TheClipboard.Close()
326
327                                 startTag = "CURA_PROFILE_STRING:"
328                                 if startTag in profileString:
329                                         #print "Found correct syntax on clipboard"
330                                         profileString = profileString.replace("\n","").strip()
331                                         profileString = profileString[profileString.find(startTag)+len(startTag):]
332                                         if profileString != self.lastTriedClipboard:
333                                                 print profileString
334                                                 self.lastTriedClipboard = profileString
335                                                 profile.setProfileFromString(profileString)
336                                                 self.scene.notification.message(_("Loaded new profile from clipboard."))
337                                                 self.updateProfileToAllControls()
338                 except:
339                         print "Unable to read from clipboard"
340
341
342         def updateSliceMode(self, changedMode = True):
343                 isSimple = profile.getPreference('startMode') == 'Simple'
344
345                 self.normalSettingsPanel.Show(not isSimple)
346                 self.simpleSettingsPanel.Show(isSimple)
347                 self.leftPane.Layout()
348
349                 for i in self.normalModeOnlyItems:
350                         i.Enable(not isSimple)
351                 if isSimple:
352                         self.switchToQuickprintMenuItem.Check()
353                 else:
354                         self.switchToNormalMenuItem.Check()
355
356                 # Set splitter sash position & size
357                 if isSimple:
358                         # Save normal mode sash (only if we changed mode from normal
359                         # to simple)
360                         if changedMode:
361                                 self.normalSashPos = self.splitter.GetSashPosition()
362
363                         # Change location of sash to width of quick mode pane
364                         (width, height) = self.simpleSettingsPanel.GetSizer().GetSize()
365                         self.splitter.SetSashPosition(width, True)
366
367                         # Disable sash
368                         self.splitter.SetSashSize(0)
369                 else:
370                         # Only change the sash position if we changed mode from simple
371                         if changedMode:
372                                 self.splitter.SetSashPosition(self.normalSashPos, True)
373                         # Enabled sash
374                         self.splitter.SetSashSize(4)
375                 self.defaultFirmwareInstallMenuItem.Enable(firmwareInstall.getDefaultFirmware() is not None)
376                 if profile.getMachineSetting('machine_type') == 'ultimaker2' or \
377                    profile.getMachineSetting('machine_type') == 'lulzbot_mini' or \
378                    profile.getMachineSetting('machine_type') == 'lulzbot_TAZ_4' or \
379                    profile.getMachineSetting('machine_type') == 'lulzbot_TAZ_5' or \
380                    profile.getMachineSetting('machine_type') == 'lulzbot_TAZ':
381                         self.bedLevelWizardMenuItem.Enable(False)
382                         self.headOffsetWizardMenuItem.Enable(False)
383                 else:
384                         self.bedLevelWizardMenuItem.Enable(True)
385                         self.headOffsetWizardMenuItem.Enable(False)
386                 if int(profile.getMachineSetting('extruder_amount')) < 2:
387                         self.headOffsetWizardMenuItem.Enable(False)
388                 self.scene.updateProfileToControls()
389                 self.scene._scene.pushFree()
390
391         def onOneAtATimeSwitch(self, e):
392                 profile.putPreference('oneAtATime', self.oneAtATime.IsChecked())
393                 if self.oneAtATime.IsChecked() and profile.getMachineSettingFloat('extruder_head_size_height') < 1:
394                         wx.MessageBox(_('For "One at a time" printing, you need to have entered the correct head size and gantry height in the machine settings'), _('One at a time warning'), wx.OK | wx.ICON_WARNING)
395                 self.scene.updateProfileToControls()
396                 self.scene._scene.pushFree()
397                 self.scene.sceneUpdated()
398
399         def OnPreferences(self, e):
400                 prefDialog = preferencesDialog.preferencesDialog(self)
401                 prefDialog.Centre()
402                 prefDialog.Show()
403                 prefDialog.Raise()
404
405         def OnMachineSettings(self, e):
406                 prefDialog = preferencesDialog.machineSettingsDialog(self)
407                 prefDialog.Centre()
408                 prefDialog.Show()
409                 prefDialog.Raise()
410
411         def OnDropFiles(self, files):
412                 self.scene.loadFiles(files)
413
414         def OnModelMRU(self, e):
415                 fileNum = e.GetId() - self.ID_MRU_MODEL1
416                 path = self.modelFileHistory.GetHistoryFile(fileNum)
417                 # Update Model MRU
418                 self.modelFileHistory.AddFileToHistory(path)  # move up the list
419                 self.config.SetPath("/ModelMRU")
420                 self.modelFileHistory.Save(self.config)
421                 self.config.Flush()
422                 # Load Model
423                 profile.putPreference('lastFile', path)
424                 filelist = [ path ]
425                 self.scene.loadFiles(filelist)
426
427         def addToModelMRU(self, file):
428                 self.modelFileHistory.AddFileToHistory(file)
429                 self.config.SetPath("/ModelMRU")
430                 self.modelFileHistory.Save(self.config)
431                 self.config.Flush()
432
433         def OnProfileMRU(self, e):
434                 fileNum = e.GetId() - self.ID_MRU_PROFILE1
435                 path = self.profileFileHistory.GetHistoryFile(fileNum)
436                 # Update Profile MRU
437                 self.profileFileHistory.AddFileToHistory(path)  # move up the list
438                 self.config.SetPath("/ProfileMRU")
439                 self.profileFileHistory.Save(self.config)
440                 self.config.Flush()
441                 # Load Profile
442                 profile.loadProfile(path)
443                 self.updateProfileToAllControls()
444
445         def addToProfileMRU(self, file):
446                 self.profileFileHistory.AddFileToHistory(file)
447                 self.config.SetPath("/ProfileMRU")
448                 self.profileFileHistory.Save(self.config)
449                 self.config.Flush()
450
451         def updateProfileToAllControls(self):
452                 self.scene.updateProfileToControls()
453                 self.normalSettingsPanel.updateProfileToControls()
454                 self.simpleSettingsPanel.updateProfileToControls()
455
456         def reloadSettingPanels(self, changedSliceMode = False):
457                 self.leftSizer.Detach(self.simpleSettingsPanel)
458                 self.leftSizer.Detach(self.normalSettingsPanel)
459                 self.simpleSettingsPanel.Destroy()
460                 self.normalSettingsPanel.Destroy()
461                 self.simpleSettingsPanel = simpleMode.simpleModePanel(self.leftPane, lambda : self.scene.sceneUpdated())
462                 self.normalSettingsPanel = normalSettingsPanel(self.leftPane, lambda : self.scene.sceneUpdated())
463                 self.leftSizer.Add(self.simpleSettingsPanel, 1)
464                 self.leftSizer.Add(self.normalSettingsPanel, 1, wx.EXPAND)
465                 self.updateSliceMode(changedSliceMode)
466                 self.updateProfileToAllControls()
467
468         def updateMachineMenu(self):
469                 #Remove all items so we can rebuild the menu. Inserting items seems to cause crashes, so this is the safest way.
470                 for item in self.machineMenu.GetMenuItems():
471                         self.machineMenu.RemoveItem(item)
472
473                 #Add a menu item for each machine configuration.
474                 for n in xrange(0, profile.getMachineCount()):
475                         i = self.machineMenu.Append(n + 0x1000, profile.getMachineSetting('machine_name', n).title(), kind=wx.ITEM_RADIO)
476                         if n == int(profile.getPreferenceFloat('active_machine')):
477                                 i.Check(True)
478                         self.Bind(wx.EVT_MENU, lambda e: self.OnSelectMachine(e.GetId() - 0x1000), i)
479
480                 self.machineMenu.AppendSeparator()
481                 i = self.machineMenu.Append(-1, _("Add new machine..."))
482                 self.Bind(wx.EVT_MENU, self.OnAddNewMachine, i)
483                 i = self.machineMenu.Append(-1, _("Machine settings..."))
484                 self.Bind(wx.EVT_MENU, self.OnMachineSettings, i)
485
486                 #Add tools for machines.
487                 self.machineMenu.AppendSeparator()
488
489                 self.defaultFirmwareInstallMenuItem = self.machineMenu.Append(-1, _("Install default firmware..."))
490                 self.Bind(wx.EVT_MENU, self.OnDefaultMarlinFirmware, self.defaultFirmwareInstallMenuItem)
491
492                 i = self.machineMenu.Append(-1, _("Install custom firmware..."))
493                 self.Bind(wx.EVT_MENU, self.OnCustomFirmware, i)
494
495         def OnLoadProfile(self, e):
496                 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)
497                 dlg.SetWildcard("ini files (*.ini)|*.ini")
498                 if dlg.ShowModal() == wx.ID_OK:
499                         profileFile = dlg.GetPath()
500                         profile.loadProfile(profileFile)
501                         self.updateProfileToAllControls()
502
503                         # Update the Profile MRU
504                         self.addToProfileMRU(profileFile)
505                 dlg.Destroy()
506
507         def OnLoadProfileFromGcode(self, e):
508                 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)
509                 dlg.SetWildcard("gcode files (*%s)|*%s;*%s" % (profile.getGCodeExtension(), profile.getGCodeExtension(), profile.getGCodeExtension()[0:2]))
510                 if dlg.ShowModal() == wx.ID_OK:
511                         gcodeFile = dlg.GetPath()
512                         f = open(gcodeFile, 'r')
513                         hasProfile = False
514                         for line in f:
515                                 if line.startswith(';CURA_PROFILE_STRING:'):
516                                         profile.setProfileFromString(line[line.find(':')+1:].strip())
517                                         if ';{profile_string}' not in profile.getAlterationFile('end.gcode'):
518                                                 profile.setAlterationFile('end.gcode', profile.getAlterationFile('end.gcode') + '\n;{profile_string}')
519                                         hasProfile = True
520                         if hasProfile:
521                                 self.updateProfileToAllControls()
522                         else:
523                                 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)
524                 dlg.Destroy()
525
526         def OnSaveProfile(self, e):
527                 dlg=wx.FileDialog(self, _("Select profile file to save"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
528                 dlg.SetWildcard("ini files (*.ini)|*.ini")
529                 if dlg.ShowModal() == wx.ID_OK:
530                         profile_filename = dlg.GetPath()
531                         if not profile_filename.lower().endswith('.ini'): #hack for linux, as for some reason the .ini is not appended.
532                                 profile_filename += '.ini'
533                         profile.saveProfile(profile_filename)
534                 dlg.Destroy()
535
536         def OnSaveDifferences(self, e):
537                 dlg=wx.FileDialog(self, _("Select profile file to save"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
538                 dlg.SetWildcard("ini files (*.ini)|*.ini")
539                 if dlg.ShowModal() == wx.ID_OK:
540                         profile_filename = dlg.GetPath()
541                         if not profile_filename.lower().endswith('.ini'): #hack for linux, as for some reason the .ini is not appended.
542                                 profile_filename += '.ini'
543                         profile.saveProfileDifferenceFromDefault(profile_filename)
544                 dlg.Destroy()
545
546         def OnResetProfile(self, e):
547                 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)
548                 result = dlg.ShowModal() == wx.ID_YES
549                 dlg.Destroy()
550                 if result:
551                         profile.resetProfile()
552                         self.updateProfileToAllControls()
553
554         def OnSimpleSwitch(self, e):
555                 profile.putPreference('startMode', 'Simple')
556                 self.updateSliceMode()
557
558         def OnNormalSwitch(self, e):
559                 profile.putPreference('startMode', 'Normal')
560                 dlg = wx.MessageDialog(self, _("Copy the settings from quickprint to your full settings?\n(This will overwrite any full setting modifications you have)"), _("Profile copy"), wx.YES_NO | wx.ICON_QUESTION)
561                 result = dlg.ShowModal() == wx.ID_YES
562                 dlg.Destroy()
563                 if result:
564                         profile.resetProfile()
565                         for k, v in self.simpleSettingsPanel.getSettingOverrides().items():
566                                 profile.putProfileSetting(k, v)
567                         self.updateProfileToAllControls()
568                 self.updateSliceMode()
569
570         def OnDefaultMarlinFirmware(self, e):
571                 firmwareInstall.InstallFirmware(self)
572
573         def OnCustomFirmware(self, e):
574                 if profile.getMachineSetting('machine_type').startswith('ultimaker'):
575                         wx.MessageBox(_("Warning: Installing a custom firmware does not guarantee that you machine will function correctly, and could damage your machine."), _("Firmware update"), wx.OK | wx.ICON_EXCLAMATION)
576                 dlg=wx.FileDialog(self, _("Open firmware to upload"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
577                 dlg.SetWildcard("HEX file (*.hex)|*.hex;*.HEX")
578                 if dlg.ShowModal() == wx.ID_OK:
579                         filename = dlg.GetPath()
580                         dlg.Destroy()
581                         if not(os.path.exists(filename)):
582                                 return
583                         #For some reason my Ubuntu 10.10 crashes here.
584                         firmwareInstall.InstallFirmware(self, filename)
585
586         def OnAddNewMachine(self, e):
587                 self.Hide()
588                 wasSimple = profile.getPreference('startMode') == 'Simple'
589                 configWizard.ConfigWizard(True)
590                 isSimple = profile.getPreference('startMode') == 'Simple'
591                 self.Show()
592                 self.reloadSettingPanels(isSimple != wasSimple)
593                 self.updateMachineMenu()
594
595         def OnSelectMachine(self, index):
596                 profile.setActiveMachine(index)
597                 self.reloadSettingPanels(False)
598
599         def OnBedLevelWizard(self, e):
600                 configWizard.bedLevelWizard()
601
602         def OnHeadOffsetWizard(self, e):
603                 configWizard.headOffsetWizard()
604
605         def OnExpertOpen(self, e):
606                 ecw = expertConfig.expertConfigWindow(lambda : self.scene.sceneUpdated())
607                 ecw.Centre()
608                 ecw.Show()
609
610         def OnMinecraftImport(self, e):
611                 mi = minecraftImport.minecraftImportWindow(self)
612                 mi.Centre()
613                 mi.Show(True)
614
615         def OnPIDDebugger(self, e):
616                 debugger = pidDebugger.debuggerWindow(self)
617                 debugger.Centre()
618                 debugger.Show(True)
619
620         def OnAutoFirmwareUpdate(self, e):
621                 dlg=wx.FileDialog(self, _("Open firmware to upload"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
622                 dlg.SetWildcard("HEX file (*.hex)|*.hex;*.HEX")
623                 if dlg.ShowModal() == wx.ID_OK:
624                         filename = dlg.GetPath()
625                         dlg.Destroy()
626                         if not(os.path.exists(filename)):
627                                 return
628                         #For some reason my Ubuntu 10.10 crashes here.
629                         installer = firmwareInstall.AutoUpdateFirmware(self, filename)
630
631         def onCopyProfileClipboard(self, e):
632                 try:
633                         if not wx.TheClipboard.IsOpened():
634                                 wx.TheClipboard.Open()
635                                 clipData = wx.TextDataObject()
636                                 self.lastTriedClipboard = profile.getProfileString()
637                                 profileString = profile.insertNewlines("CURA_PROFILE_STRING:" + self.lastTriedClipboard)
638                                 clipData.SetText(profileString)
639                                 wx.TheClipboard.SetData(clipData)
640                                 wx.TheClipboard.Close()
641                 except:
642                         print "Could not write to clipboard, unable to get ownership. Another program is using the clipboard."
643
644         def OnCheckForUpdate(self, e):
645                 newVersion = version.checkForNewerVersion()
646                 if newVersion is not None:
647                         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:
648                                 webbrowser.open(newVersion)
649                 else:
650                         wx.MessageBox(_("You are running the latest version of Cura!"), _("Awesome!"), wx.ICON_INFORMATION)
651
652         def OnAbout(self, e):
653                 aboutBox = aboutWindow.aboutWindow(self)
654                 aboutBox.Centre()
655                 aboutBox.Show()
656                 aboutBox.Raise()
657
658         def OnClose(self, e):
659                 profile.saveProfile(profile.getDefaultProfilePath(), True)
660
661                 # Save the window position, size & state from the preferences file
662                 profile.putPreference('window_maximized', self.IsMaximized())
663                 if not self.IsMaximized() and not self.IsIconized():
664                         (posx, posy) = self.GetPosition()
665                         profile.putPreference('window_pos_x', posx)
666                         profile.putPreference('window_pos_y', posy)
667                         (width, height) = self.GetSize()
668                         profile.putPreference('window_width', width)
669                         profile.putPreference('window_height', height)
670
671                         # Save normal sash position.  If in normal mode (!simple mode), get last position of sash before saving it...
672                         isSimple = profile.getPreference('startMode') == 'Simple'
673                         if not isSimple:
674                                 self.normalSashPos = self.splitter.GetSashPosition()
675                         profile.putPreference('window_normal_sash', self.normalSashPos)
676
677                 #HACK: Set the paint function of the glCanvas to nothing so it won't keep refreshing. Which can keep wxWidgets from quiting.
678                 print "Closing down"
679                 self.scene.OnPaint = lambda e : e
680                 self.scene.cleanup()
681                 self.Destroy()
682
683         def OnQuit(self, e):
684                 self.Close()
685
686 class normalSettingsPanel(configBase.configPanelBase):
687         "Main user interface window"
688         def __init__(self, parent, callback = None):
689                 super(normalSettingsPanel, self).__init__(parent, callback)
690
691                 #Main tabs
692                 self.nb = wx.Notebook(self)
693                 self.SetSizer(wx.BoxSizer(wx.HORIZONTAL))
694                 self.GetSizer().Add(self.nb, 1, wx.EXPAND)
695
696                 (left, right, self.printPanel) = self.CreateDynamicConfigTab(self.nb, _('Basic'))
697                 self._addSettingsToPanels('basic', left, right)
698                 self.SizeLabelWidths(left, right)
699
700                 (left, right, self.advancedPanel) = self.CreateDynamicConfigTab(self.nb, _('Advanced'))
701                 self._addSettingsToPanels('advanced', left, right)
702                 self.SizeLabelWidths(left, right)
703
704                 #Plugin page
705                 self.pluginPanel = pluginPanel.pluginPanel(self.nb, callback)
706                 self.nb.AddPage(self.pluginPanel, _("Plugins"))
707
708                 #Alteration page
709                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
710                         self.alterationPanel = None
711                 else:
712                         self.alterationPanel = alterationPanel.alterationPanel(self.nb, callback)
713                         self.nb.AddPage(self.alterationPanel, _("Start/End-GCode"))
714
715                 self.Bind(wx.EVT_SIZE, self.OnSize)
716
717                 self.nb.SetSize(self.GetSize())
718                 self.UpdateSize(self.printPanel)
719                 self.UpdateSize(self.advancedPanel)
720
721         def _addSettingsToPanels(self, category, left, right):
722                 count = len(profile.getSubCategoriesFor(category)) + len(profile.getSettingsForCategory(category))
723
724                 p = left
725                 n = 0
726                 for title in profile.getSubCategoriesFor(category):
727                         n += 1 + len(profile.getSettingsForCategory(category, title))
728                         if n > count / 2:
729                                 p = right
730                         configBase.TitleRow(p, _(title))
731                         for s in profile.getSettingsForCategory(category, title):
732                                 configBase.SettingRow(p, s.getName())
733
734         def SizeLabelWidths(self, left, right):
735                 leftWidth = self.getLabelColumnWidth(left)
736                 rightWidth = self.getLabelColumnWidth(right)
737                 maxWidth = max(leftWidth, rightWidth)
738                 self.setLabelColumnWidth(left, maxWidth)
739                 self.setLabelColumnWidth(right, maxWidth)
740
741         def OnSize(self, e):
742                 # Make the size of the Notebook control the same size as this control
743                 self.nb.SetSize(self.GetSize())
744
745                 # Propegate the OnSize() event (just in case)
746                 e.Skip()
747
748                 # Perform out resize magic
749                 self.UpdateSize(self.printPanel)
750                 self.UpdateSize(self.advancedPanel)
751
752         def UpdateSize(self, configPanel):
753                 sizer = configPanel.GetSizer()
754
755                 # Pseudocde
756                 # if horizontal:
757                 #     if width(col1) < best_width(col1) || width(col2) < best_width(col2):
758                 #         switch to vertical
759                 # else:
760                 #     if width(col1) > (best_width(col1) + best_width(col1)):
761                 #         switch to horizontal
762                 #
763
764                 col1 = configPanel.leftPanel
765                 colSize1 = col1.GetSize()
766                 colBestSize1 = col1.GetBestSize()
767                 col2 = configPanel.rightPanel
768                 colSize2 = col2.GetSize()
769                 colBestSize2 = col2.GetBestSize()
770
771                 orientation = sizer.GetOrientation()
772
773                 if orientation == wx.HORIZONTAL:
774                         if (colSize1[0] <= colBestSize1[0]) or (colSize2[0] <= colBestSize2[0]):
775                                 configPanel.Freeze()
776                                 sizer = wx.BoxSizer(wx.VERTICAL)
777                                 sizer.Add(configPanel.leftPanel, flag=wx.EXPAND)
778                                 sizer.Add(configPanel.rightPanel, flag=wx.EXPAND)
779                                 configPanel.SetSizer(sizer)
780                                 #sizer.Layout()
781                                 configPanel.Layout()
782                                 self.Layout()
783                                 configPanel.Thaw()
784                 else:
785                         if max(colSize1[0], colSize2[0]) > (colBestSize1[0] + colBestSize2[0]):
786                                 configPanel.Freeze()
787                                 sizer = wx.BoxSizer(wx.HORIZONTAL)
788                                 sizer.Add(configPanel.leftPanel, proportion=1, border=35, flag=wx.EXPAND)
789                                 sizer.Add(configPanel.rightPanel, proportion=1, flag=wx.EXPAND)
790                                 configPanel.SetSizer(sizer)
791                                 #sizer.Layout()
792                                 configPanel.Layout()
793                                 self.Layout()
794                                 configPanel.Thaw()
795
796         def updateProfileToControls(self):
797                 super(normalSettingsPanel, self).updateProfileToControls()
798                 if self.alterationPanel is not None:
799                         self.alterationPanel.updateProfileToControls()
800                 self.pluginPanel.updateProfileToControls()