# This file is part of MyPaint. # Copyright (C) 2007 by Martin Renold # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. "window to model a single brush property function" from gettext import gettext as _ import gtk from brushlib import brushsettings import windowing class Window(windowing.SubWindow): def __init__(self, app, setting, adj): windowing.SubWindow.__init__(self, app, key_input=True) self.app.brushmanager.selected_brush_observers.append(self.brush_selected_cb) self.set_title(setting.name) self.connect('delete-event', self.app.hide_window_cb) scroll = gtk.ScrolledWindow() scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) self.add(scroll) vbox = gtk.VBox() vbox.set_spacing(5) scroll.add_with_viewport(vbox) vbox.set_border_width(5) l = gtk.Label() l.set_alignment(0.0, 0.0) l.set_markup('%s' % setting.name) l.set_tooltip_text(setting.tooltip) vbox.pack_start(l, expand=False) l = gtk.Label() l.set_markup(_('Base Value')) l.set_alignment(0.0, 0.0) l.xpad = 5 vbox.pack_start(l, expand=False) hbox = gtk.HBox() vbox.pack_start(hbox, expand=False) scale = self.base_value_hscale = gtk.HScale(adj) scale.set_digits(2) scale.set_draw_value(True) scale.set_value_pos(gtk.POS_LEFT) hbox.pack_start(scale, expand=True) b = gtk.Button("%.1f" % setting.default) b.connect('clicked', self.set_fixed_value_clicked_cb, adj, setting.default) hbox.pack_start(b, expand=False) vbox.pack_start(gtk.HSeparator(), expand=False) self.byinputwidgets = [] for i in brushsettings.inputs: w = ByInputWidget(self.app, i, setting) self.byinputwidgets.append(w) vbox.pack_start(w, expand=False) vbox.pack_start(gtk.HSeparator(), expand=False) # functionwindows are a little narrower and a little taller than # their parent brushsettingswindow, and will appear over the top # of it. Positions are not saved, though they'll hide and show with # tab self.set_default_size(440, 550) def set_fixed_value_clicked_cb(self, widget, adj, value): adj.set_value(value) def brush_selected_cb(self, base_brush, settings_str=None): # update curves for w in self.byinputwidgets: w.reread() class ByInputWidget(gtk.VBox): "the gui elements that change the response to one input" def __init__(self, app, input, setting): gtk.VBox.__init__(self) self.set_spacing(5) self.app = app self.input = input self.setting = setting self.block_user_changes_cb = True lower = -20.0 upper = +20.0 if input.hard_min is not None: lower = input.hard_min if input.hard_max is not None: upper = input.hard_max self.xmin_adj = gtk.Adjustment(value=input.soft_min, lower=lower, upper=upper-0.1, step_incr=0.01, page_incr=0.1) self.xmax_adj = gtk.Adjustment(value=input.soft_max, lower=lower+0.1, upper=upper, step_incr=0.01, page_incr=0.1) diff = setting.max - setting.min self.scale_y_adj = gtk.Adjustment(value=diff/4.0, lower=-diff, upper=+diff, step_incr=0.01, page_incr=0.1) self.xmin_adj.connect('value-changed', self.user_changes_cb) self.xmax_adj.connect('value-changed', self.user_changes_cb) self.scale_y_adj.connect('value-changed', self.user_changes_cb) l = gtk.Label() l.set_markup(input.dname) l.set_alignment(0.0, 0.0) l.set_tooltip_text(input.tooltip) self.pack_start(l, expand=False) hbox = gtk.HBox() self.pack_start(hbox, expand=False) #hbox.pack_start(gtk.Label('by ' + input.name), expand=False) scale = self.base_value_hscale = gtk.HScale(self.scale_y_adj) scale.set_digits(2) scale.set_draw_value(True) scale.set_value_pos(gtk.POS_LEFT) hbox.pack_start(scale, expand=True) b = gtk.Button("0.0") b.connect('clicked', self.set_fixed_value_clicked_cb, self.scale_y_adj, 0.0) hbox.pack_start(b, expand=False) t = gtk.Table(4, 4) c = self.curve_widget = CurveWidget(self.user_changes_cb) t.attach(c, 0, 3, 0, 3, gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL, 5, 0) l1 = gtk.SpinButton(self.scale_y_adj); l1.set_digits(2) l2 = gtk.Label('+0.0') l3 = gtk.Label() def update_negative_scale(*trash): l3.set_text('%+.2f' % -self.scale_y_adj.get_value()) self.scale_y_adj.connect('value-changed', update_negative_scale) update_negative_scale() t.attach(l1, 3, 4, 0, 1, 0, 0, 5, 0) t.attach(l2, 3, 4, 1, 2, 0, gtk.EXPAND, 5, 0) t.attach(l3, 3, 4, 2, 3, 0, 0, 5, 0) l4 = gtk.SpinButton(self.xmin_adj); l4.set_digits(2) l5 = gtk.Label('') l6 = gtk.SpinButton(self.xmax_adj); l6.set_digits(2) t.attach(l4, 0, 1, 3, 4, 0, 0, 5, 0) t.attach(l5, 1, 2, 3, 4, gtk.EXPAND, 0, 5, 0) t.attach(l6, 2, 3, 3, 4, 0, 0, 5, 0) expander = self.expander = gtk.Expander(label=_('Details')) expander.add(t) expander.set_expanded(False) self.pack_start(expander, expand=False) self.reread() def set_fixed_value_clicked_cb(self, widget, adj, value): adj.set_value(value) def user_changes_cb(self, widget): if self.block_user_changes_cb: return # 1. verify and constrain the changes scale_y = self.scale_y_adj.get_value() xmax = self.xmax_adj.get_value() xmin = self.xmin_adj.get_value() if xmax <= xmin: # change the other one if widget is self.xmax_adj: self.xmin_adj.set_value(xmax - 0.1) elif widget is self.xmin_adj: self.xmax_adj.set_value(xmin + 0.1) else: assert False return # this function caused itself to be called assert xmax > xmin # 2. update the brush if scale_y: brush_points = [self.point_widget2real(p) for p in self.curve_widget.points] nonzero = [True for x, y in brush_points if y != 0] if not nonzero: brush_points = [] else: brush_points = [] self.app.brush.settings[self.setting.index].set_points(self.input, brush_points) # 3. update display self.update_graypoint() def update_graypoint(self): # highlight (x_normal, 0) # the user should make the curve go through this point self.curve_widget.graypoint = (self.get_x_normal(), 0.5) self.curve_widget.queue_draw() def reconsider_details(self): s = self.app.brush.settings[self.setting.index] if s.has_input_nonlinear(self.input): self.expander.set_expanded(True) else: self.expander.set_expanded(False) def point_real2widget(self, (x, y)): scale_y = self.scale_y_adj.get_value() xmax = self.xmax_adj.get_value() xmin = self.xmin_adj.get_value() scale_x = xmax - xmin assert scale_x if scale_y == 0: y = None else: y = -(y/scale_y/2.0)+0.5 x = (x-xmin)/scale_x return (x, y) def point_widget2real(self, (x, y)): scale_y = self.scale_y_adj.get_value() xmax = self.xmax_adj.get_value() xmin = self.xmin_adj.get_value() scale_x = xmax - xmin x = xmin + (x * scale_x) y = (0.5-y) * 2.0 * scale_y return (x, y) def get_x_normal(self): # return widget x coordinate of the "normal" input value return self.point_real2widget((self.input.normal, 0.0))[0] def reread(self): brush_points = self.app.brush.settings[self.setting.index].points[self.input.index] brush_points_zero = [(self.input.soft_min, 0.0), (self.input.soft_max, 0.0)] if not brush_points: brush_points = brush_points_zero xmin, xmax = brush_points[0][0], brush_points[-1][0] assert xmax > xmin assert max([x for x, y in brush_points]) == xmax assert min([x for x, y in brush_points]) == xmin y_min = min([y for x, y in brush_points]) y_max = max([y for x, y in brush_points]) scale_y = max(abs(y_min), abs(y_max)) # choose between scale_y and -scale_y (arbitrary) if brush_points[0][1] > brush_points[-1][1]: scale_y = -scale_y if not scale_y: brush_points = brush_points_zero # if xmin/xmax were non-default, reset them xmin = self.input.soft_min xmax = self.input.soft_max # xmin, xmax, scale_y are fixed now self.block_user_changes_cb = True self.xmax_adj.set_value(xmax) self.xmin_adj.set_value(xmin) self.scale_y_adj.set_value(scale_y) self.block_user_changes_cb = False curve_points = [self.point_real2widget(p) for p in brush_points] if not scale_y: # note, curve_points has undefined y values (None) y0 = -1.0 y1 = +1.0 x_normal = self.get_x_normal() # the default line should go through zero at x_normal # change one of the border points to achieve this if x_normal >= 0.0 and x_normal <= 1.0: if x_normal < 0.5: y0 = -0.5/(x_normal-1.0) y1 = 0.0 else: y0 = 1.0 y1 = -0.5/x_normal + 1.0 (x0, trash0), (x1, trash1) = curve_points curve_points = [(x0, y0), (x1, y1)] self.curve_widget.points = curve_points self.curve_widget.queue_draw() self.update_graypoint() self.reconsider_details() RADIUS = 4 class CurveWidget(gtk.DrawingArea): "modify a (restricted) nonlinear curve" def __init__(self, changed_cb, magnetic=True): gtk.DrawingArea.__init__(self) self.points = [(0.0, 0.5), (1.0, 0.0)] # doesn't matter self.maxpoints = 8 self.grabbed = None self.changed_cb = changed_cb self.magnetic = magnetic self.connect("expose-event", self.expose_cb) self.connect("button-press-event", self.button_press_cb) self.connect("button-release-event", self.button_release_cb) self.connect("motion-notify-event", self.motion_notify_cb) self.set_events(gtk.gdk.EXPOSURE_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK ) self.set_size_request(300, 200) self.graypoint = None def eventpoint(self, event_x, event_y): # FIXME: very ugly; and code duplication, see expose_cb width, height = self.window.get_size() width -= 2*RADIUS height -= 2*RADIUS width = width / 4 * 4 height = height / 4 * 4 x, y = event_x, event_y x -= RADIUS y -= RADIUS x = float(x) / width y = float(y) / height return (x, y) def button_press_cb(self, widget, event): if not event.button == 1: return x, y = self.eventpoint(event.x, event.y) nearest = None for i in range(len(self.points)): px, py = self.points[i] dist = abs(px - x) + 0.5*abs(py - y) if nearest is None or dist < mindist: mindist = dist nearest = i if mindist > 0.05 and len(self.points) < self.maxpoints: insertpos = 0 for i in range(len(self.points)): if self.points[i][0] < x: insertpos = i + 1 if insertpos > 0 and insertpos < len(self.points): if y > 1.0: y = 1.0 if y < 0.0: y = 0.0 self.points.insert(insertpos, (x, y)) nearest = insertpos self.queue_draw() #if nearest == 0: # # first point cannot be grabbed # display = gtk.gdk.display_get_default() # display.beep() #else: #assert self.grabbed is None # This did happen. I think it's save to ignore? # I guess it's because gtk can generate button press event without corresponding release. self.grabbed = nearest def button_release_cb(self, widget, event): if not event.button == 1: return if self.grabbed: i = self.grabbed if self.points[i] is None: self.points.pop(i) self.grabbed = None # notify user of the widget self.changed_cb(self) def motion_notify_cb(self, widget, event): if self.grabbed is None: return x, y = self.eventpoint(event.x, event.y) i = self.grabbed out = False # by default, the point cannot be removed by drawing it out if i == len(self.points)-1: # last point stays right leftbound = rightbound = 1.0 elif i == 0: # first point stays left leftbound = rightbound = 0.0 else: # other points can be dragged out if y > 1.1 or y < -0.1: out = True leftbound = self.points[i-1][0] rightbound = self.points[i+1][0] if x <= leftbound - 0.02 or x >= rightbound + 0.02: out = True if out: self.points[i] = None else: if y > 1.0: y = 1.0 if y < 0.0: y = 0.0 if self.magnetic: if y > 0.48 and y < 0.52: y = 0.5 if x > 0.48 and x < 0.52: x = 0.5 if x < leftbound: x = leftbound if x > rightbound: x = rightbound self.points[i] = (x, y) self.queue_draw() def expose_cb(self, widget, event): window = widget.window state = gtk.STATE_NORMAL gray = widget.style.bg_gc[state] dark = widget.style.dark_gc[state] black = widget.style.fg_gc[state] width, height = window.get_size() window.draw_rectangle(gray, True, 0, 0, width, height) width -= 2*RADIUS height -= 2*RADIUS width = width / 4 * 4 height = height / 4 * 4 if width <= 0 or height <= 0: return # draw grid lines for i in range(5): window.draw_line(dark, RADIUS, i*height/4 + RADIUS, width + RADIUS, i*height/4 + RADIUS) window.draw_line(dark, i*width/4 + RADIUS, RADIUS, i*width/4 + RADIUS, height + RADIUS) if self.graypoint: x1, y1 = self.graypoint x1 = int(x1*width) + RADIUS y1 = int(y1*height) + RADIUS window.draw_rectangle(dark, True, x1-RADIUS-1, y1-RADIUS-1, 2*RADIUS+1, 2*RADIUS+1) # draw points for p in self.points: if p is None: continue x1, y1 = p x1 = int(x1*width) + RADIUS y1 = int(y1*height) + RADIUS window.draw_rectangle(black, True, x1-RADIUS-1, y1-RADIUS-1, 2*RADIUS+1, 2*RADIUS+1) if p is not self.points[0]: window.draw_line(black, x0, y0, x1, y1) x0, y0 = x1, y1 return True