3 # Copyright (C) 2014 Antonio Ospite <ao2@ao2.it>
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 from wx.lib.pubsub import setuparg1
20 from wx.lib.pubsub import pub
21 from wx.lib.wordwrap import wordwrap
26 ICON_FILE = "res/ao2.ico"
33 class CanvasFrame(wx.Frame):
34 def __init__(self, *args, **kwargs):
35 base_image = kwargs.pop('base_image', None)
36 wx.Frame.__init__(self, *args, **kwargs)
38 # Set up a sizer BEFORE every other action, in order to prevent any
39 # weird side effects; for instance self.SetToolBar() seems to resize
41 vsizer = wx.BoxSizer(orient=wx.VERTICAL)
44 # Instantiate the Model and set up the view
45 self.model = CanvasModel.Canvas(IMAGE_WIDTH, IMAGE_HEIGHT,
48 self.view = CanvasView.CanvasView(self, model=self.model,
49 base_image=base_image)
50 vsizer.Add(self.view, 0, wx.SHAPED)
52 icon = wx.Icon(ICON_FILE, wx.BITMAP_TYPE_ICO)
56 menu_bar = self._BuildMenu()
57 self.SetMenuBar(menu_bar)
60 tool_bar = self._BuildTools()
61 self.SetToolBar(tool_bar)
64 status_bar = wx.StatusBar(self)
65 status_bar.SetWindowStyle(status_bar.GetWindowStyle() ^ wx.ST_SIZEGRIP)
66 status_bar.SetFieldsCount(3)
67 self.SetStatusBar(status_bar)
70 pub.subscribe(self.UpdateStatusBar, "NEW PIXEL")
71 pub.subscribe(self.UpdateView, "NEW PIXEL")
74 self.view.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
75 self.view.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
76 self.view.Bind(wx.EVT_MOTION, self.OnMouseMotion)
79 self.Bind(wx.EVT_CLOSE, self.OnQuit)
81 # The frame gets resized to fit all its elements
82 self.GetSizer().Fit(self)
83 # and centered on screen
84 self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
86 def _BuildTools(self):
87 tool_bar = wx.ToolBar(self, style=wx.TB_HORZ_LAYOUT|wx.TB_TEXT)
89 color_picker_label = wx.StaticText(tool_bar, label=" Color picker ")
90 tool_bar.AddControl(color_picker_label)
94 ID_COLOR_PICKER = wx.NewId()
95 color_picker = wx.ColourPickerCtrl(tool_bar, ID_COLOR_PICKER, self.color,
98 tool_bar.AddControl(color_picker)
99 wx.EVT_COLOURPICKER_CHANGED(self, ID_COLOR_PICKER, self.OnPickColor)
101 tool_bar.AddSeparator()
103 ID_SHOW_GRID = wx.NewId()
104 show_grid_checkbox = wx.CheckBox(tool_bar, ID_SHOW_GRID, label="Show grid", style=wx.ALIGN_RIGHT)
105 show_grid_checkbox.SetValue(self.view.draw_grid)
106 tool_bar.AddControl(show_grid_checkbox)
107 wx.EVT_CHECKBOX(tool_bar, ID_SHOW_GRID, self.OnShowGrid)
113 def _BuildMenu(self):
114 menu_bar = wx.MenuBar()
117 file_menu = wx.Menu()
118 menu_bar.Append(file_menu, '&File')
120 ID_NEW_BITMAP = wx.ID_NEW
121 file_menu.Append(ID_NEW_BITMAP, 'New Bitmap', 'Start a new bitmap')
122 wx.EVT_MENU(self, ID_NEW_BITMAP, self.OnNewBitmap)
124 ID_LOAD_BITMAP = wx.ID_OPEN
125 file_menu.Append(ID_LOAD_BITMAP, 'Load Bitmap', 'Load a bitmap')
126 wx.EVT_MENU(self, ID_LOAD_BITMAP, self.OnLoadBitmap)
128 ID_SAVE_BITMAP = wx.ID_SAVE
129 file_menu.Append(ID_SAVE_BITMAP, 'Save Bitmap', 'Save a bitmap')
130 wx.EVT_MENU(self, ID_SAVE_BITMAP, self.OnSaveBitmap)
132 file_menu.AppendSeparator()
135 export_menu = wx.Menu()
136 file_menu.AppendMenu(wx.ID_ANY, 'E&xport', export_menu)
138 ID_EXPORT_ANIMATION = wx.NewId()
139 export_menu.Append(ID_EXPORT_ANIMATION, 'Export animation', 'Export as animation')
140 wx.EVT_MENU(self, ID_EXPORT_ANIMATION, self.OnExportAnimation)
142 ID_EXPORT_SNAPSHOT = wx.NewId()
143 export_menu.Append(ID_EXPORT_SNAPSHOT, 'Export snapshot',
144 'Export a snapshot of the current canvas')
145 wx.EVT_MENU(self, ID_EXPORT_SNAPSHOT, self.OnExportSnapshot)
147 # Last item of file_menu
148 ID_EXIT_MENUITEM = wx.ID_EXIT
149 file_menu.Append(ID_EXIT_MENUITEM, 'E&xit\tAlt-X', 'Exit the program')
150 wx.EVT_MENU(self, ID_EXIT_MENUITEM, self.OnQuit)
153 help_menu = wx.Menu()
154 menu_bar.Append(help_menu, '&Help')
156 ID_HELP_MENUITEM = wx.ID_HELP
157 help_menu.Append(ID_HELP_MENUITEM, 'About\tAlt-A', 'Show Informations')
158 wx.EVT_MENU(self, ID_HELP_MENUITEM, self.ShowAboutDialog)
162 def addPixel(self, event):
163 x, y = event.GetLogicalPosition(self.view.dc)
164 self.SetStatusText("Last Click at %-3d,%-3d" % (x, y), 0)
166 r, theta = CanvasView.cartesian2polar(x, y, self.view.offset_angle)
167 self.model.setPixelColor(r, theta, self.color)
169 def OnNewBitmap(self, event):
170 if self.ShowConfirmationDialog() == wx.ID_YES:
172 self.view.drawAllPixels()
175 def OnLoadBitmap(self, event):
176 dialog = wx.FileDialog(self, "Load bitmap", "", "",
177 "PNG files (*.png)|*.png",
178 wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
179 ret = dialog.ShowModal()
180 file_path = dialog.GetPath()
183 if ret == wx.ID_CANCEL:
186 if self.view.loadImage(file_path):
187 self.view.drawAllPixels()
190 self.ShowErrorDialog("Image is not %dx%d" % (self.model.width,
193 def OnSaveBitmap(self, event):
194 dialog = wx.FileDialog(self, "Save bitmap", "", "",
195 "PNG files (*.png)|*.png",
196 wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
197 ret = dialog.ShowModal()
198 file_path = dialog.GetPath()
201 if ret == wx.ID_CANCEL:
204 bitmap = wx.BitmapFromBuffer(self.model.width, self.model.height,
205 self.model.pixels_array)
206 bitmap.SaveFile(file_path, wx.BITMAP_TYPE_PNG)
208 def OnExportAnimation(self, event):
209 dialog = wx.FileDialog(self, "Save animation", "", "animation.h",
210 "C header files (*.h)|*.h",
211 wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
212 ret = dialog.ShowModal()
213 file_path = dialog.GetPath()
216 if ret == wx.ID_CANCEL:
219 self.model.saveAsAnimation(file_path)
221 def OnExportSnapshot(self, event):
222 dialog = wx.FileDialog(self, "Take snapwhot", "", "snapshot.png",
223 "PNG files (*.png)|*.png",
224 wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
225 ret = dialog.ShowModal()
226 file_path = dialog.GetPath()
229 if ret == wx.ID_CANCEL:
232 self.view.pixels_buffer.SaveFile(file_path, wx.BITMAP_TYPE_PNG)
234 def OnQuit(self, event):
235 if self.ShowConfirmationDialog("Exit the program?") == wx.ID_YES:
238 def OnPickColor(self, event):
239 self.color = event.Colour.asTuple()
241 def OnShowGrid(self, event):
242 self.view.draw_grid = event.Checked()
245 def OnLeftDown(self, event):
247 self.view.CaptureMouse()
249 def OnLeftUp(self, event):
250 self.view.ReleaseMouse()
252 def OnMouseMotion(self, event):
253 if event.Dragging() and event.LeftIsDown():
256 def UpdateStatusBar(self, event):
257 if self.model.last_pixel:
258 x, y = self.model.last_pixel
259 r, theta = self.model.toPolar(x, y)
260 self.SetStatusText("r: %-4.1f theta: %-4.1f" % (r, theta), 1)
261 self.SetStatusText("x: %-2d y: %-2d" % (x, y), 2)
263 def UpdateView(self, event):
264 if self.model.last_pixel:
265 self.view.drawPixel(self.model.last_pixel)
268 def ShowConfirmationDialog(self, message=None):
270 message = "With this operation you can loose your data.\n\n"
271 message += "Are you really sure you want to proceed?"
273 dialog = wx.MessageDialog(self, message, "Warning!",
274 style=wx.YES_NO | wx.ICON_QUESTION)
275 ret = dialog.ShowModal()
280 def ShowErrorDialog(self, message):
281 dialog = wx.MessageDialog(self, message, "Error!",
282 style=wx.OK | wx.ICON_ERROR)
283 ret = dialog.ShowModal()
286 def ShowAboutDialog(self, event):
287 info = wx.AboutDialogInfo()
288 info.Name = "PoPiPaint - Polar Pixel Painter"
289 info.Copyright = "(C) 2014 Antonio Ospite"
290 text = "A prototype program for the JMPrope project,"
291 text += "the programmable jump rope with LEDs."
292 info.Description = wordwrap(text, 350, wx.ClientDC(self))
293 info.WebSite = ("http://ao2.it", "http://ao2.it")
294 info.Developers = ["Antonio Ospite"]
295 info.License = "GNU/GPLv3"