OSDN Git Service

a3aaeca3f8d1fbfa4e9d2cc9d3074bc7c589b7d5
[mypaint-anime/master.git] / gui / application.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2007 by Martin Renold <martinxyz@gmx.ch>
3 #
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.
8
9 import os, sys
10 from os.path import join
11 import gtk, gobject
12 gdk = gtk.gdk
13 from lib import brush, helpers, mypaintlib
14 import filehandling, keyboard, brushmanager, windowing, document, layout
15 import colorhistory, brushmodifier
16 import stock
17
18 class Application: # singleton
19     """
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.
23     """
24     def __init__(self, datapath, confpath, filenames):
25         self.confpath = confpath
26         self.datapath = datapath
27
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):
33                 os.mkdir(d)
34                 print 'Created', d
35
36         self.ui_manager = gtk.UIManager()
37
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')
46
47         stock.init_custom_stock_items()
48
49         gdk.set_program_class('MyPaint')
50
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)
53
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()
58
59         self.preferences = {}
60         self.load_settings()
61
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)
67         
68         self.filehandler.scratchpad_filename = ""
69         self.filehandler.scratchpad_doc = document.Scratchpad(self)
70         
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"]
74
75         self.brush.set_color_hsv((0, 0, 0))
76         self.init_brush_adjustments()
77
78         self.ch = colorhistory.ColorHistory(self)
79
80         self.layout_manager = layout.LayoutManager(
81             prefs=self.preferences["layout.window_positions"],
82             factory=windowing.window_factory,
83             factory_opts=[self]  )
84         self.drawWindow = self.layout_manager.get_widget_by_role("main-window")
85         self.layout_manager.show_all()
86
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'))
91         
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")
97
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")
101
102         def at_application_start(*junk):
103             self.brushmanager.select_initial_brush()
104             if filenames:
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
110                 else:
111                     self.filehandler.open_file(fn)
112
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)
119
120             self.apply_settings()
121             if not self.pressure_devices:
122                 print 'No pressure sensitive devices found.'
123             self.drawWindow.present()
124
125         gobject.idle_add(at_application_start)
126
127     def save_settings(self):
128         """Saves the current settings to persistent storage."""
129         def save_config():
130             settingspath = join(self.confpath, 'settings.json')
131             jsonstr = helpers.json_dumps(self.preferences)
132             f = open(settingspath, 'w')
133             f.write(jsonstr)
134             f.close()
135         self.brushmanager.save_brushes_for_devices()
136         self.filehandler.save_scratchpad(self.filehandler.scratchpad_filename)
137         save_config()
138
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()
145
146     def load_settings(self):
147         '''Loads the settings from persistent storage. Uses defaults if
148         not explicitly configured'''
149         def get_legacy_config():
150             dummyobj = {}
151             tmpdict = {}
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']
158             return tmpdict
159         def get_json_config():
160             settingspath = join(self.confpath, 'settings.json')
161             jsonstr = open(settingspath).read()
162             try:
163                 return helpers.json_loads(jsonstr)
164             except Exception, e:
165                 print "settings.json: %s" % (str(e),)
166                 print "warning: failed to load settings.json, using defaults"
167                 return {}
168         if sys.platform == 'win32':
169             import glib
170             scrappre = join(glib.get_user_special_dir(glib.USER_DIRECTORY_DOCUMENTS).decode('utf-8'),'MyPaint','scrap')
171         else:
172             scrappre = '~/MyPaint/scrap'
173         DEFAULT_CONFIG = {
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,
182             'ui.toolbar': 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',
194
195             "scratchpad.last_opened_scratchpad": "",
196
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": {
201
202                 # Main window default size. Sidebar width is saved here
203                 'main-window': dict(sbwidth=270, x=64, y=32, w=-74, h=-96),
204
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,
210                                            x=-200, y=128,
211                                            w=200, h=300, sbheight=300),
212                 'colorSelectionWindow': dict(sbindex=0, floating=True, hidden=True,
213                                              x=-128, y=64,
214                                              w=200, h=250, sbheight=250),
215                 'brushSelectionWindow': dict(sbindex=2, floating=True,
216                                              x=-128, y=-128,
217                                              w=250, h=350, sbheight=350),
218                 'layersWindow': dict(sbindex=3, floating=True,
219                                      x=128, y=-128,
220                                      w=200, h=200, sbheight=200),
221
222                 'scratchWindow': dict(sbindex=4, floating=True,
223                                      x=128, y=-128,
224                                      w=200, h=200, sbheight=200),
225
226                 # Non-tool subwindows. These cannot be docked, and are all
227                 # intially hidden.
228                 'brushSettingsWindow': dict(x=-460, y=-128, w=300, h=300),
229                 'backgroundWindow': dict(),
230                 'inputTestWindow': dict(),
231                 'frameWindow': dict(),
232                 'preferencesWindow': dict(),
233             },
234         }
235         window_pos = DEFAULT_CONFIG["layout.window_positions"]
236         self.window_names = window_pos.keys()
237         self.preferences = DEFAULT_CONFIG
238         try:
239             user_config = get_json_config()
240         except IOError:
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
251
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
259
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
265         else:
266             # TODO: maybe replace this stupid mapping by a hard<-->soft slider?
267             m = mypaintlib.Mapping(1)
268             m.set_n(0, len(p))
269             for i, (x, y) in enumerate(p):
270                 m.set_point(0, i, x, 1.0-y)
271
272             def mapping(pressure):
273                 return m.calculate_single_input(pressure)
274             self.doc.tdw.pressure_mapping = mapping
275
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
281
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.
286
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.
293                 #
294                 # device.name is usually something like "wacom intuos3 6x8 pad" or just "pad"
295                 print 'Ignoring "%s" (probably wacom keypad device)' % device.name
296                 continue
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
300                 continue
301
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
309                         continue
310
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)
317                     break
318
319     def save_gui_config(self):
320         gtk.accel_map_save(join(self.confpath, 'accelmap.conf'))
321         self.save_settings()
322
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)
326         d.set_markup(text)
327         d.run()
328         d.destroy()
329
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()
339         image = None
340         x = x_root-size/2
341         y = y_root-size/2
342         if x < 0: x = 0
343         if y < 0: y = 0
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])
353         N = size*size
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)
357
358 class PixbufDirectory:
359     def __init__(self, dirname):
360         self.dirname = dirname
361         self.cache = {}
362
363     def __getattr__(self, name):
364         if name not in self.cache:
365             try:
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]