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
17 class Application: # singleton
19 This class serves as a global container for everything that needs
20 to be shared in the GUI. Its constructor is the last part of the
21 initialization, called by main.py or by the testing scripts.
23 def __init__(self, datapath, confpath, filenames):
24 self.confpath = confpath
25 self.datapath = datapath
27 # create config directory, and subdirs where the user might drop files
28 for d in ['', 'backgrounds', 'brushes']:
29 d = os.path.join(self.confpath, d)
30 if not os.path.isdir(d):
34 self.ui_manager = gtk.UIManager()
36 # if we are not installed, use the icons from the source
37 theme = gtk.icon_theme_get_default()
38 themedir_src = join(self.datapath, 'desktop/icons')
39 theme.prepend_search_path(themedir_src)
40 if not theme.has_icon('mypaint'):
41 print 'Warning: Where have all my icons gone?'
42 print 'Theme search path:', theme.get_search_path()
43 gtk.window_set_default_icon_name('mypaint')
45 gdk.set_program_class('MyPaint')
47 self.pixmaps = PixbufDirectory(join(self.datapath, 'pixmaps'))
48 self.cursor_color_picker = gdk.Cursor(gdk.display_get_default(), self.pixmaps.cursor_color_picker, 1, 30)
50 # unmanaged main brush; always the same instance (we can attach settings_observers)
51 # this brush is where temporary changes (color, size...) happen
52 self.brush = brush.Brush()
57 self.brushmanager = brushmanager.BrushManager(join(datapath, 'brushes'), join(confpath, 'brushes'), self)
58 self.kbm = keyboard.KeyboardManager()
59 self.filehandler = filehandling.FileHandler(self)
60 self.doc = document.Document(self)
62 self.set_current_brush(self.brushmanager.selected_brush)
63 self.brush.set_color_hsv((0, 0, 0))
64 self.brushmanager.selected_brush_observers.append(self.brush_selected_cb)
65 self.init_brush_adjustments()
67 self.ch = colorhistory.ColorHistory(self)
69 self.layout_manager = layout.LayoutManager(
70 prefs=self.preferences["layout.window_positions"],
71 factory=windowing.window_factory,
73 self.drawWindow = self.layout_manager.get_widget_by_role("main-window")
74 self.layout_manager.show_all()
76 self.kbm.start_listening()
77 self.filehandler.doc = self.doc
78 self.filehandler.filename = None
79 gtk.accel_map_load(join(self.confpath, 'accelmap.conf'))
81 # Load the background settings window.
82 # FIXME: this line shouldn't be needed, but we need to load this up
83 # front to get any non-default background that the user has configured
84 # from the preferences.
85 self.layout_manager.get_subwindow_by_role("backgroundWindow")
87 # And the brush settings window, or things like eraser mode will break.
88 # FIXME: brush_adjustments should not be dependent on this
89 self.layout_manager.get_subwindow_by_role("brushSettingsWindow")
91 def at_application_start(*trash):
93 # Open only the first file, no matter how many has been specified
94 # If the file does not exist just set it as the file to save to
95 fn = filenames[0].replace('file:///', '/') # some filebrowsers do this (should only happen with outdated mypaint.desktop)
96 if not os.path.exists(fn):
97 self.filehandler.filename = fn
99 self.filehandler.open_file(fn)
101 self.apply_settings()
102 if not self.pressure_devices:
103 print 'No pressure sensitive devices found.'
104 self.drawWindow.present()
106 gobject.idle_add(at_application_start)
108 def save_settings(self):
109 """Saves the current settings to persistent storage."""
111 settingspath = join(self.confpath, 'settings.json')
112 jsonstr = helpers.json_dumps(self.preferences)
113 f = open(settingspath, 'w')
116 self.brushmanager.save_brushes_for_devices()
119 def apply_settings(self):
120 """Applies the current settings."""
121 self.update_input_mapping()
122 self.update_input_devices()
123 prefs_win = self.layout_manager.get_widget_by_role('preferencesWindow')
124 prefs_win.update_ui()
126 def load_settings(self):
127 '''Loads the settings from persistent storage. Uses defaults if
128 not explicitly configured'''
129 def get_legacy_config():
132 settingspath = join(self.confpath, 'settings.conf')
133 if os.path.exists(settingspath):
134 exec open(settingspath) in dummyobj
135 tmpdict['saving.scrap_prefix'] = dummyobj['save_scrap_prefix']
136 tmpdict['input.device_mode'] = dummyobj['input_devices_mode']
137 tmpdict['input.global_pressure_mapping'] = dummyobj['global_pressure_mapping']
139 def get_json_config():
140 settingspath = join(self.confpath, 'settings.json')
141 jsonstr = open(settingspath).read()
142 return helpers.json_loads(jsonstr)
143 if sys.platform == 'win32':
145 scrappre = join(glib.get_user_special_dir(glib.USER_DIRECTORY_DOCUMENTS).decode('utf-8'),'MyPaint','scrap')
147 scrappre = '~/MyPaint/scrap'
149 'saving.scrap_prefix': scrappre,
150 'input.device_mode': 'screen',
151 'input.global_pressure_mapping': [(0.0, 1.0), (1.0, 0.0)],
152 'view.default_zoom': 1.0,
153 'saving.default_format': 'openraster',
154 'brushmanager.selected_brush' : None,
155 'brushmanager.selected_groups' : [],
156 "input.button1_shift_action": 'straight_line',
157 "input.button1_ctrl_action": 'ColorPickerPopup',
158 "input.button2_action": 'pan_canvas',
159 "input.button2_shift_action": 'rotate_canvas',
160 "input.button2_ctrl_action": 'zoom_canvas',
161 "input.button3_action": 'ColorHistoryPopup',
162 "input.button3_shift_action": 'no_action',
163 "input.button3_ctrl_action": 'no_action',
165 # Default window positions.
166 # See gui.layout.set_window_initial_position for the meanings
167 # of the common x, y, w, and h settings
168 "layout.window_positions": {
170 # Main window default size. Sidebar width is saved here
171 'main-window': dict(sbwidth=270, x=64, y=32, w=-74, h=-96),
173 # Tool windows. These can be undocked (floating=True) or set
174 # initially hidden (hidden=True), or be given an initial sidebar
175 # index (sbindex=<int>) or height in the sidebar (sbheight=<int>)
176 # Non-hidden entries determine the default set of tools.
177 'colorSamplerWindow': dict(sbindex=0, floating=True,
179 w=200, h=300, sbheight=300),
180 'colorSelectionWindow': dict(sbindex=1, floating=True, hidden=True,
182 w=150, h=150, sbheight=150),
183 'brushSelectionWindow': dict(sbindex=2, floating=True,
185 w=250, h=350, sbheight=350),
186 'layersWindow': dict(sbindex=3, floating=True,
188 w=200, h=200, sbheight=200),
190 # Non-tool subwindows. These cannot be docked, and are all
192 'brushSettingsWindow': dict(x=-460, y=-128, w=300, h=300),
195 self.preferences = DEFAULT_CONFIG
197 user_config = get_json_config()
199 user_config = get_legacy_config()
200 self.preferences.update(user_config)
202 def init_brush_adjustments(self, ):
203 """Initializes all the brush adjustments for the current brush"""
204 self.brush_adjustment = {}
205 from brushlib import brushsettings
206 for i, s in enumerate(brushsettings.settings_visible):
207 adj = gtk.Adjustment(value=s.default, lower=s.min, upper=s.max, step_incr=0.01, page_incr=0.1)
208 self.brush_adjustment[s.cname] = adj
210 def update_input_mapping(self):
211 p = self.preferences['input.global_pressure_mapping']
212 if len(p) == 2 and abs(p[0][1]-1.0)+abs(p[1][1]-0.0) < 0.0001:
213 # 1:1 mapping (mapping disabled)
214 self.doc.tdw.pressure_mapping = None
216 # TODO: maybe replace this stupid mapping by a hard<-->soft slider?
217 m = mypaintlib.Mapping(1)
219 for i, (x, y) in enumerate(p):
220 m.set_point(0, i, x, 1.0-y)
222 def mapping(pressure):
223 return m.calculate_single_input(pressure)
224 self.doc.tdw.pressure_mapping = mapping
226 def update_input_devices(self):
227 # init extended input devices
228 self.pressure_devices = []
229 for device in gdk.devices_list():
230 #print device.name, device.source
232 #if device.source in [gdk.SOURCE_PEN, gdk.SOURCE_ERASER]:
233 # The above contition is True sometimes for a normal USB
234 # Mouse. https://gna.org/bugs/?11215
235 # In fact, GTK also just guesses this value from device.name.
237 last_word = device.name.split()[-1].lower()
238 if last_word == 'pad':
239 # Setting the intuos3 pad into "screen mode" causes
240 # glitches when you press a pad-button in mid-stroke,
241 # and it's not a pointer device anyway. But it reports
242 # axes almost identical to the pen and eraser.
244 # device.name is usually something like "wacom intuos3 6x8 pad" or just "pad"
245 print 'Ignoring "%s" (probably wacom keypad device)' % device.name
247 if last_word == 'cursor':
248 # this is a "normal" mouse and does not work in screen mode
249 print 'Ignoring "%s" (probably wacom mouse device)' % device.name
252 for use, val_min, val_max in device.axes:
253 # Some mice have a third "pressure" axis, but without
254 # minimum or maximum. https://gna.org/bugs/?14029
255 if use == gdk.AXIS_PRESSURE and val_min != val_max:
256 if 'mouse' in device.name.lower():
257 # Real fix for the above bug https://gna.org/bugs/?14029
258 print 'Ignoring "%s" (probably a mouse, but it reports extra axes)' % device.name
261 self.pressure_devices.append(device.name)
262 modesetting = self.preferences['input.device_mode']
263 mode = getattr(gdk, 'MODE_' + modesetting.upper())
264 if device.mode != mode:
265 print 'Setting %s mode for "%s"' % (modesetting, device.name)
266 device.set_mode(mode)
269 def set_current_brush(self, managed_brush):
271 Copies a ManagedBrush's settings into the brush settings currently used
272 for painting. Sets the parent brush name to the closest ancestor brush
273 currently in the brushlist.
275 if managed_brush is None:
277 self.brush.load_from_brushinfo(managed_brush.brushinfo)
279 list_brush = self.brushmanager.find_brushlist_ancestor(managed_brush)
280 if list_brush and list_brush.name is not None:
281 parent_name = list_brush.name
282 self.brush.brushinfo["parent_brush_name"] = parent_name
284 def brush_selected_cb(self, brush):
285 assert brush is not self.brush
286 self.set_current_brush(brush)
288 def save_gui_config(self):
289 gtk.accel_map_save(join(self.confpath, 'accelmap.conf'))
292 def message_dialog(self, text, type=gtk.MESSAGE_INFO, flags=0):
293 """utility function to show a message/information dialog"""
294 d = gtk.MessageDialog(self.drawWindow, flags=flags, buttons=gtk.BUTTONS_OK, type=type)
299 def pick_color_at_pointer(self, widget, size=3):
300 '''Grab screen color at cursor (average of size x size rectangle)'''
301 # inspired by gtkcolorsel.c function grab_color_at_mouse()
302 screen = widget.get_screen()
303 colormap = screen.get_system_colormap()
304 root = screen.get_root_window()
305 screen_w, screen_h = screen.get_width(), screen.get_height()
306 display = widget.get_display()
307 screen_trash, x_root, y_root, modifiermask_trash = display.get_pointer()
313 if x+size > screen_w: x = screen_w-size
314 if y+size > screen_h: y = screen_h-size
315 image = root.get_image(x, y, size, size)
316 color_total = (0, 0, 0)
317 for x, y in helpers.iter_rect(0, 0, size, size):
318 pixel = image.get_pixel(x, y)
319 color = colormap.query_color(pixel)
320 color = [color.red, color.green, color.blue]
321 color_total = (color_total[0]+color[0], color_total[1]+color[1], color_total[2]+color[2])
323 color_total = (color_total[0]/N, color_total[1]/N, color_total[2]/N)
324 color_rgb = [ch/65535. for ch in color_total]
325 self.brush.set_color_rgb(color_rgb)
327 class PixbufDirectory:
328 def __init__(self, dirname):
329 self.dirname = dirname
332 def __getattr__(self, name):
333 if name not in self.cache:
335 pixbuf = gdk.pixbuf_new_from_file(join(self.dirname, name + '.png'))
336 except gobject.GError, e:
337 raise AttributeError, str(e)
338 self.cache[name] = pixbuf
339 return self.cache[name]