1 # This file is part of MyPaint.
2 # Copyright (C) 2007 by Martin Renold <martinxyz@gmx.ch>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
10 from os.path import join
13 from lib import brush, helpers, mypaintlib
14 import filehandling, keyboard, brushmanager, windowing, document, layout
15 import colorhistory, brushmodifier
18 class Application: # singleton
20 This class serves as a global container for everything that needs
21 to be shared in the GUI. Its constructor is the last part of the
22 initialization, called by main.py or by the testing scripts.
24 def __init__(self, datapath, confpath, filenames):
25 self.confpath = confpath
26 self.datapath = datapath
28 # create config directory, and subdirs where the user might drop files
29 # TODO make scratchpad dir something pulled from preferences #PALETTE1
30 for d in ['', 'backgrounds', 'brushes', 'scratchpads']:
31 d = os.path.join(self.confpath, d)
32 if not os.path.isdir(d):
36 self.ui_manager = gtk.UIManager()
38 # if we are not installed, use the icons from the source
39 theme = gtk.icon_theme_get_default()
40 themedir_src = join(self.datapath, 'desktop/icons')
41 theme.prepend_search_path(themedir_src)
42 if not theme.has_icon('mypaint'):
43 print 'Warning: Where have all my icons gone?'
44 print 'Theme search path:', theme.get_search_path()
45 gtk.window_set_default_icon_name('mypaint')
47 stock.init_custom_stock_items()
49 gdk.set_program_class('MyPaint')
51 self.pixmaps = PixbufDirectory(join(self.datapath, 'pixmaps'))
52 self.cursor_color_picker = gdk.Cursor(gdk.display_get_default(), self.pixmaps.cursor_color_picker, 1, 30)
54 # unmanaged main brush; always the same instance (we can attach settings_observers)
55 # this brush is where temporary changes (color, size...) happen
56 self.brush = brush.BrushInfo()
57 self.brush.load_defaults()
62 self.brushmanager = brushmanager.BrushManager(join(datapath, 'brushes'), join(confpath, 'brushes'), self)
63 self.kbm = keyboard.KeyboardManager()
64 self.filehandler = filehandling.FileHandler(self)
65 self.brushmodifier = brushmodifier.BrushModifier(self)
66 self.doc = document.Document(self)
68 self.filehandler.scratchpad_filename = ""
69 self.filehandler.scratchpad_doc = document.Scratchpad(self)
71 if not self.preferences.get("scratchpad.last_opened_scratchpad", None):
72 self.preferences["scratchpad.last_opened_scratchpad"] = self.filehandler.get_scratchpad_autosave()
73 self.filehandler.scratchpad_filename = self.preferences["scratchpad.last_opened_scratchpad"]
75 self.brush.set_color_hsv((0, 0, 0))
76 self.init_brush_adjustments()
78 self.ch = colorhistory.ColorHistory(self)
80 self.layout_manager = layout.LayoutManager(
81 prefs=self.preferences["layout.window_positions"],
82 factory=windowing.window_factory,
84 self.drawWindow = self.layout_manager.get_widget_by_role("main-window")
85 self.layout_manager.show_all()
87 self.kbm.start_listening()
88 self.filehandler.doc = self.doc
89 self.filehandler.filename = None
90 gtk.accel_map_load(join(self.confpath, 'accelmap.conf'))
92 # Load the background settings window.
93 # FIXME: this line shouldn't be needed, but we need to load this up
94 # front to get any non-default background that the user has configured
95 # from the preferences.
96 self.layout_manager.get_subwindow_by_role("backgroundWindow")
98 # And the brush settings window, or things like eraser mode will break.
99 # FIXME: brush_adjustments should not be dependent on this
100 self.layout_manager.get_subwindow_by_role("brushSettingsWindow")
102 def at_application_start(*junk):
103 self.brushmanager.select_initial_brush()
105 # Open only the first file, no matter how many has been specified
106 # If the file does not exist just set it as the file to save to
107 fn = filenames[0].replace('file:///', '/') # some filebrowsers do this (should only happen with outdated mypaint.desktop)
108 if not os.path.exists(fn):
109 self.filehandler.filename = fn
111 self.filehandler.open_file(fn)
113 # Load last scratchpad
114 if not self.preferences["scratchpad.last_opened_scratchpad"]:
115 self.preferences["scratchpad.last_opened_scratchpad"] = self.filehandler.get_scratchpad_autosave()
116 self.filehandler.scratchpad_filename = self.preferences["scratchpad.last_opened_scratchpad"]
117 if os.path.isfile(self.filehandler.scratchpad_filename):
118 self.filehandler.open_scratchpad(self.filehandler.scratchpad_filename)
120 self.apply_settings()
121 if not self.pressure_devices:
122 print 'No pressure sensitive devices found.'
123 self.drawWindow.present()
125 gobject.idle_add(at_application_start)
127 def save_settings(self):
128 """Saves the current settings to persistent storage."""
130 settingspath = join(self.confpath, 'settings.json')
131 jsonstr = helpers.json_dumps(self.preferences)
132 f = open(settingspath, 'w')
135 self.brushmanager.save_brushes_for_devices()
136 self.filehandler.save_scratchpad(self.filehandler.scratchpad_filename)
139 def apply_settings(self):
140 """Applies the current settings."""
141 self.update_input_mapping()
142 self.update_input_devices()
143 prefs_win = self.layout_manager.get_widget_by_role('preferencesWindow')
144 prefs_win.update_ui()
146 def load_settings(self):
147 '''Loads the settings from persistent storage. Uses defaults if
148 not explicitly configured'''
149 def get_legacy_config():
152 settingspath = join(self.confpath, 'settings.conf')
153 if os.path.exists(settingspath):
154 exec open(settingspath) in dummyobj
155 tmpdict['saving.scrap_prefix'] = dummyobj['save_scrap_prefix']
156 tmpdict['input.device_mode'] = dummyobj['input_devices_mode']
157 tmpdict['input.global_pressure_mapping'] = dummyobj['global_pressure_mapping']
159 def get_json_config():
160 settingspath = join(self.confpath, 'settings.json')
161 jsonstr = open(settingspath).read()
163 return helpers.json_loads(jsonstr)
165 print "settings.json: %s" % (str(e),)
166 print "warning: failed to load settings.json, using defaults"
168 if sys.platform == 'win32':
170 scrappre = join(glib.get_user_special_dir(glib.USER_DIRECTORY_DOCUMENTS).decode('utf-8'),'MyPaint','scrap')
172 scrappre = '~/MyPaint/scrap'
174 'saving.scrap_prefix': scrappre,
175 'input.device_mode': 'screen',
176 'input.global_pressure_mapping': [(0.0, 1.0), (1.0, 0.0)],
177 'view.default_zoom': 1.0,
178 'view.high_quality_zoom': True,
179 'ui.hide_menubar_in_fullscreen': True,
180 'ui.hide_toolbar_in_fullscreen': True,
181 'ui.hide_subwindows_in_fullscreen': True,
183 'saving.default_format': 'openraster',
184 'brushmanager.selected_brush' : None,
185 'brushmanager.selected_groups' : [],
186 "input.button1_shift_action": 'straight_line',
187 "input.button1_ctrl_action": 'ColorPickerPopup',
188 "input.button2_action": 'pan_canvas',
189 "input.button2_shift_action": 'rotate_canvas',
190 "input.button2_ctrl_action": 'zoom_canvas',
191 "input.button3_action": 'ColorHistoryPopup',
192 "input.button3_shift_action": 'no_action',
193 "input.button3_ctrl_action": 'no_action',
195 "scratchpad.last_opened_scratchpad": "",
197 # Default window positions.
198 # See gui.layout.set_window_initial_position for the meanings
199 # of the common x, y, w, and h settings
200 "layout.window_positions": {
202 # Main window default size. Sidebar width is saved here
203 'main-window': dict(sbwidth=270, x=64, y=32, w=-74, h=-96),
205 # Tool windows. These can be undocked (floating=True) or set
206 # initially hidden (hidden=True), or be given an initial sidebar
207 # index (sbindex=<int>) or height in the sidebar (sbheight=<int>)
208 # Non-hidden entries determine the default set of tools.
209 'colorSamplerWindow': dict(sbindex=1, floating=False, hidden=False,
211 w=200, h=300, sbheight=300),
212 'colorSelectionWindow': dict(sbindex=0, floating=True, hidden=True,
214 w=200, h=250, sbheight=250),
215 'brushSelectionWindow': dict(sbindex=2, floating=True,
217 w=250, h=350, sbheight=350),
218 'layersWindow': dict(sbindex=3, floating=True,
220 w=200, h=200, sbheight=200),
222 'scratchWindow': dict(sbindex=4, floating=True,
224 w=200, h=200, sbheight=200),
226 # Non-tool subwindows. These cannot be docked, and are all
228 'brushSettingsWindow': dict(x=-460, y=-128, w=300, h=300),
229 'backgroundWindow': dict(),
230 'inputTestWindow': dict(),
231 'frameWindow': dict(),
232 'preferencesWindow': dict(),
235 window_pos = DEFAULT_CONFIG["layout.window_positions"]
236 self.window_names = window_pos.keys()
237 self.preferences = DEFAULT_CONFIG
239 user_config = get_json_config()
241 user_config = get_legacy_config()
242 user_window_pos = user_config.get("layout.window_positions", {})
243 # note: .update() replaces the window position dict, but we want to update it
244 self.preferences.update(user_config)
245 # update window_pos, and drop window names that don't exist any more
246 # (we need to drop them because otherwise we will try to show a non-existing window)
247 for role in self.window_names:
248 if role in user_window_pos:
249 window_pos[role] = user_window_pos[role]
250 self.preferences["layout.window_positions"] = window_pos
252 def init_brush_adjustments(self):
253 """Initializes all the brush adjustments for the current brush"""
254 self.brush_adjustment = {}
255 from brushlib import brushsettings
256 for i, s in enumerate(brushsettings.settings_visible):
257 adj = gtk.Adjustment(value=s.default, lower=s.min, upper=s.max, step_incr=0.01, page_incr=0.1)
258 self.brush_adjustment[s.cname] = adj
260 def update_input_mapping(self):
261 p = self.preferences['input.global_pressure_mapping']
262 if len(p) == 2 and abs(p[0][1]-1.0)+abs(p[1][1]-0.0) < 0.0001:
263 # 1:1 mapping (mapping disabled)
264 self.doc.tdw.pressure_mapping = None
266 # TODO: maybe replace this stupid mapping by a hard<-->soft slider?
267 m = mypaintlib.Mapping(1)
269 for i, (x, y) in enumerate(p):
270 m.set_point(0, i, x, 1.0-y)
272 def mapping(pressure):
273 return m.calculate_single_input(pressure)
274 self.doc.tdw.pressure_mapping = mapping
276 def update_input_devices(self):
277 # init extended input devices
278 self.pressure_devices = []
279 for device in gdk.devices_list():
280 #print device.name, device.source
282 #if device.source in [gdk.SOURCE_PEN, gdk.SOURCE_ERASER]:
283 # The above contition is True sometimes for a normal USB
284 # Mouse. https://gna.org/bugs/?11215
285 # In fact, GTK also just guesses this value from device.name.
287 last_word = device.name.split()[-1].lower()
288 if last_word == 'pad':
289 # Setting the intuos3 pad into "screen mode" causes
290 # glitches when you press a pad-button in mid-stroke,
291 # and it's not a pointer device anyway. But it reports
292 # axes almost identical to the pen and eraser.
294 # device.name is usually something like "wacom intuos3 6x8 pad" or just "pad"
295 print 'Ignoring "%s" (probably wacom keypad device)' % device.name
297 if last_word == 'cursor':
298 # this is a "normal" mouse and does not work in screen mode
299 print 'Ignoring "%s" (probably wacom mouse device)' % device.name
302 for use, val_min, val_max in device.axes:
303 # Some mice have a third "pressure" axis, but without
304 # minimum or maximum. https://gna.org/bugs/?14029
305 if use == gdk.AXIS_PRESSURE and val_min != val_max:
306 if 'mouse' in device.name.lower():
307 # Real fix for the above bug https://gna.org/bugs/?14029
308 print 'Ignoring "%s" (probably a mouse, but it reports extra axes)' % device.name
311 self.pressure_devices.append(device.name)
312 modesetting = self.preferences['input.device_mode']
313 mode = getattr(gdk, 'MODE_' + modesetting.upper())
314 if device.mode != mode:
315 print 'Setting %s mode for "%s"' % (modesetting, device.name)
316 device.set_mode(mode)
319 def save_gui_config(self):
320 gtk.accel_map_save(join(self.confpath, 'accelmap.conf'))
323 def message_dialog(self, text, type=gtk.MESSAGE_INFO, flags=0):
324 """utility function to show a message/information dialog"""
325 d = gtk.MessageDialog(self.drawWindow, flags=flags, buttons=gtk.BUTTONS_OK, type=type)
330 def pick_color_at_pointer(self, widget, size=3):
331 '''Grab screen color at cursor (average of size x size rectangle)'''
332 # inspired by gtkcolorsel.c function grab_color_at_mouse()
333 screen = widget.get_screen()
334 colormap = screen.get_system_colormap()
335 root = screen.get_root_window()
336 screen_w, screen_h = screen.get_width(), screen.get_height()
337 display = widget.get_display()
338 screen_junk, x_root, y_root, modifiermask_trash = display.get_pointer()
344 if x+size > screen_w: x = screen_w-size
345 if y+size > screen_h: y = screen_h-size
346 image = root.get_image(x, y, size, size)
347 color_total = (0, 0, 0)
348 for x, y in helpers.iter_rect(0, 0, size, size):
349 pixel = image.get_pixel(x, y)
350 color = colormap.query_color(pixel)
351 color = [color.red, color.green, color.blue]
352 color_total = (color_total[0]+color[0], color_total[1]+color[1], color_total[2]+color[2])
354 color_total = (color_total[0]/N, color_total[1]/N, color_total[2]/N)
355 color_rgb = [ch/65535. for ch in color_total]
356 self.brush.set_color_rgb(color_rgb)
358 class PixbufDirectory:
359 def __init__(self, dirname):
360 self.dirname = dirname
363 def __getattr__(self, name):
364 if name not in self.cache:
366 pixbuf = gdk.pixbuf_new_from_file(join(self.dirname, name + '.png'))
367 except gobject.GError, e:
368 raise AttributeError, str(e)
369 self.cache[name] = pixbuf
370 return self.cache[name]