diff options
Diffstat (limited to 'extra/arandr/arandr-0.1.3/screenlayout/widget.py')
-rw-r--r-- | extra/arandr/arandr-0.1.3/screenlayout/widget.py | 406 |
1 files changed, 406 insertions, 0 deletions
diff --git a/extra/arandr/arandr-0.1.3/screenlayout/widget.py b/extra/arandr/arandr-0.1.3/screenlayout/widget.py new file mode 100644 index 000000000..391d33779 --- /dev/null +++ b/extra/arandr/arandr-0.1.3/screenlayout/widget.py @@ -0,0 +1,406 @@ +from __future__ import division +import os +import stat +import pango +import pangocairo +import gobject, gtk +from .auxiliary import Position, Size, NORMAL, ROTATIONS, InadequateConfiguration +from .xrandr import XRandR +from .snap import Snap + +import gettext +gettext.install('arandr') + +class ARandRWidget(gtk.DrawingArea): + __gsignals__ = { + 'expose-event':'override', # FIXME: still needed? + 'changed':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + + def __init__(self, factor=8, display=None, force_version=False): + super(ARandRWidget, self).__init__() + + self._factor = factor + + self.set_size_request(1024//self.factor, 1024//self.factor) # best guess for now + + self.connect('button-press-event', self.click) + self.set_events(gtk.gdk.BUTTON_PRESS_MASK) + + self.setup_draganddrop() + + self._xrandr = XRandR(display=display, force_version=force_version) + + #################### widget features #################### + + def _set_factor(self, f): + self._factor = f + self._update_size_request() + self._force_repaint() + + factor = property(lambda self: self._factor, _set_factor) + + def abort_if_unsafe(self): + if not len([x for x in self._xrandr.configuration.outputs.values() if x.active]): + d = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_YES_NO, _("Your configuration does not include an active monitor. Do you want to apply the configuration?")) + result = d.run() + d.destroy() + if result == gtk.RESPONSE_YES: + return False + else: + return True + return False + + def error_message(self, message): + d = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, message) + d.run() + d.destroy() + + def _update_size_request(self): + max_gapless = sum(max(o.size) if o.active else 0 for o in self._xrandr.configuration.outputs.values()) # this ignores that some outputs might not support rotation, but will always err at the side of caution. + # have some buffer + usable_size = int(max_gapless * 1.1) + # don't request too large a window, but make sure very possible compination fits + xdim = min(self._xrandr.state.virtual.max[0], usable_size) + ydim = min(self._xrandr.state.virtual.max[1], usable_size) + self.set_size_request(xdim//self.factor, ydim//self.factor) + + #################### loading #################### + + def load_from_file(self, file): + data = open(file).read() + template = self._xrandr.load_from_string(data) + self._xrandr_was_reloaded() + return template + + def load_from_x(self): + self._xrandr.load_from_x() + self._xrandr_was_reloaded() + return self._xrandr.DEFAULTTEMPLATE + + def _xrandr_was_reloaded(self): + self.sequence = sorted(self._xrandr.outputs) + self._lastclick = (-1,-1) + + self._update_size_request() + if self.window: + self._force_repaint() + self.emit('changed') + + def save_to_x(self): + self._xrandr.save_to_x() + self.load_from_x() + + def save_to_file(self, file, template=None, additional=None): + data = self._xrandr.save_to_shellscript_string(template, additional) + open(file, 'w').write(data) + os.chmod(file, stat.S_IRWXU) + self.load_from_file(file) + + #################### doing changes #################### + + def _set_something(self, which, on, data): + old = getattr(self._xrandr.configuration.outputs[on], which) + setattr(self._xrandr.configuration.outputs[on], which, data) + try: + self._xrandr.check_configuration() + except InadequateConfiguration: + setattr(self._xrandr.configuration.outputs[on], which, old) + raise + + self._force_repaint() + self.emit('changed') + + def set_position(self, on, pos): + self._set_something('position', on, pos) + def set_rotation(self, on, rot): + self._set_something('rotation', on, rot) + def set_resolution(self, on, res): + self._set_something('mode', on, res) + + def set_active(self, on, active): + v = self._xrandr.state.virtual + o = self._xrandr.configuration.outputs[on] + + if not active and o.active: + o.active = False + # don't delete: allow user to re-enable without state being lost + if active and not o.active: + if hasattr(o, 'position'): + o.active = True # nothing can go wrong, position already set + else: + pos = Position((0,0)) + for m in self._xrandr.state.outputs[on].modes: + # determine first possible mode + if m[0]<=v.max[0] and m[1]<=v.max[1]: + mode = m + break + else: + raise InadequateConfiguration("Smallest mode too large for virtual.") + + o.active = True + o.position = pos + o.mode = mode + o.rotation = NORMAL + + self._force_repaint() + self.emit('changed') + + #################### painting #################### + + def do_expose_event(self, event): + cr = pangocairo.CairoContext(self.window.cairo_create()) + cr.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) + cr.clip() + + # clear + cr.set_source_rgb(0,0,0) + cr.rectangle(0,0,*self.window.get_size()) + cr.fill() + cr.save() + + cr.scale(1/self.factor, 1/self.factor) + cr.set_line_width(self.factor*1.5) + + self._draw(self._xrandr, cr) + + def _draw(self, xrandr, cr): + cfg = xrandr.configuration + state = xrandr.state + + cr.set_source_rgb(0.25,0.25,0.25) + cr.rectangle(0,0,*state.virtual.max) + cr.fill() + + cr.set_source_rgb(0.5,0.5,0.5) + cr.rectangle(0,0,*cfg.virtual) + cr.fill() + + for on in self.sequence: + o = cfg.outputs[on] + if not o.active: continue + + rect = (o.tentative_position if hasattr(o, 'tentative_position') else o.position) + o.size + center = rect[0]+rect[2]/2, rect[1]+rect[3]/2 + + # paint rectangle + cr.set_source_rgba(1,1,1,0.7) + cr.rectangle(*rect) + cr.fill() + cr.set_source_rgb(0,0,0) + cr.rectangle(*rect) + cr.stroke() + + # set up for text + cr.save() + textwidth = rect[3 if o.rotation.is_odd else 2] + widthperchar = textwidth/len(on) + textheight = int(widthperchar * 0.8) # i think this looks nice and won't overflow even for wide fonts + + newdescr = pango.FontDescription("sans") + newdescr.set_size(textheight * pango.SCALE) + + # create text + layout = cr.create_layout() + layout.set_font_description(newdescr) + layout.set_text(on) + + # position text + layoutsize = layout.get_pixel_size() + layoutoffset = -layoutsize[0]/2, -layoutsize[1]/2 + cr.move_to(*center) + cr.rotate(o.rotation.angle) + cr.rel_move_to(*layoutoffset) + + # pain text + cr.show_layout(layout) + cr.restore() + + def _force_repaint(self): + # using self.allocation as rect is offset by the menu bar. + self.window.invalidate_rect(gtk.gdk.Rectangle(0,0,self._xrandr.state.virtual.max[0]//self.factor,self._xrandr.state.virtual.max[1]//self.factor), False) + # this has the side effect of not painting out of the available region on drag and drop + + #################### click handling #################### + + def click(self, widget, event): + undermouse = self._get_point_outputs(event.x, event.y) + if event.button == 1 and undermouse: + which = self._get_point_active_output(event.x, event.y) + if self._lastclick == (event.x, event.y): # this was the second click to that stack + # push the highest of the undermouse windows below the lowest + newpos = min(self.sequence.index(a) for a in undermouse) + self.sequence.remove(which) + self.sequence.insert(newpos,which) + # sequence changed + which = self._get_point_active_output(event.x, event.y) + # pull the clicked window to the absolute top + self.sequence.remove(which) + self.sequence.append(which) + + self._lastclick = (event.x, event.y) + self._force_repaint() + if event.button == 3: + if undermouse: + target = [a for a in self.sequence if a in undermouse][-1] + m = self._contextmenu(target) + m.popup(None, None, None, event.button, event.time) + else: + m = self.contextmenu() + m.popup(None, None, None, event.button, event.time) + + self._lastclick = (event.x, event.y) # deposit for drag and drop until better way found to determine exact starting coordinates + + def _get_point_outputs(self, x, y): + x,y = x*self.factor, y*self.factor + outputs = set() + for on,o in self._xrandr.configuration.outputs.items(): + if not o.active: continue + if o.position[0]-self.factor <= x <= o.position[0]+o.size[0]+self.factor and o.position[1]-self.factor <= y <= o.position[1]+o.size[1]+self.factor: + outputs.add(on) + return outputs + + def _get_point_active_output(self, x, y): + undermouse = self._get_point_outputs(x, y) + if not undermouse: raise IndexError("No output here.") + active = [a for a in self.sequence if a in undermouse][-1] + return active + + #################### context menu #################### + + def contextmenu(self): + m = gtk.Menu() + for on in self._xrandr.outputs: + i = gtk.MenuItem(on) + i.props.submenu = self._contextmenu(on) + m.add(i) + m.show_all() + return m + + def _contextmenu(self, on): + m = gtk.Menu() + oc = self._xrandr.configuration.outputs[on] + os = self._xrandr.state.outputs[on] + + enabled = gtk.CheckMenuItem(_("Active")) + enabled.props.active = oc.active + if not oc.active and not os.connected: + enabled.props.sensitive = False + enabled.connect('activate', lambda menuitem: self.set_active(on, menuitem.props.active)) + + m.add(enabled) + + if oc.active: + res_m = gtk.Menu() + for r in os.modes: + i = gtk.CheckMenuItem("%sx%s"%r) + i.props.draw_as_radio = True + i.props.active = (oc.mode == r) + def _res_set(menuitem, on, r): + try: + self.set_resolution(on, r) + except InadequateConfiguration, e: + self.error_message(_("Setting this resolution is not possible here: %s")%e.message) + i.connect('activate', _res_set, on, r) + res_m.add(i) + + or_m = gtk.Menu() + for r in ROTATIONS: + i = gtk.CheckMenuItem("%s"%r) + i.props.draw_as_radio = True + i.props.active = (oc.rotation == r) + def _rot_set(menuitem, on, r): + try: + self.set_rotation(on, r) + except InadequateConfiguration, e: + self.error_message(_("This orientation is not possible here: %s")%e.message) + i.connect('activate', _rot_set, on, r) + if r not in os.rotations: + i.props.sensitive = False + or_m.add(i) + + res_i = gtk.MenuItem(_("Resolution")) + res_i.props.submenu = res_m + or_i = gtk.MenuItem(_("Orientation")) + or_i.props.submenu = or_m + + m.add(res_i) + m.add(or_i) + + m.show_all() + return m + + #################### drag&drop #################### + + def setup_draganddrop(self): + self.drag_source_set(gtk.gdk.BUTTON1_MASK, [('screenlayout-output', gtk.TARGET_SAME_WIDGET, 0)], 0) + self.drag_dest_set(0, [('screenlayout-output', gtk.TARGET_SAME_WIDGET, 0)], 0) + #self.drag_source_set(gtk.gdk.BUTTON1_MASK, [], 0) + #self.drag_dest_set(0, [], 0) + + self._draggingfrom = None + self._draggingoutput = None + self.connect('drag-begin', self._dragbegin_cb) + self.connect('drag-motion', self._dragmotion_cb) + self.connect('drag-drop', self._dragdrop_cb) + self.connect('drag-end', self._dragend_cb) + + self._lastclick = (0,0) + + def _dragbegin_cb(self, widget, context): + try: + output = self._get_point_active_output(*self._lastclick) + except IndexError: + # FIXME: abort? + context.set_icon_stock(gtk.STOCK_CANCEL, 10,10) + return None + + self._draggingoutput = output + self._draggingfrom = self._lastclick + context.set_icon_stock(gtk.STOCK_FULLSCREEN, 10,10) + + self._draggingsnap = Snap( + self._xrandr.configuration.outputs[self._draggingoutput].size, + self.factor*5, + [(Position((0,0)),self._xrandr.state.virtual.max)]+[ + (v.position, v.size) for (k,v) in self._xrandr.configuration.outputs.items() if k!=self._draggingoutput and v.active + ] + ) + + def _dragmotion_cb(self, widget, context, x, y, time): + if not 'screenlayout-output' in context.targets: # from outside + return False + if not self._draggingoutput: # from void; should be already aborted + return False + + context.drag_status(gtk.gdk.ACTION_MOVE, time) + + rel = x-self._draggingfrom[0], y-self._draggingfrom[1] + + oldpos = self._xrandr.configuration.outputs[self._draggingoutput].position + newpos = Position((oldpos[0]+self.factor*rel[0], oldpos[1]+self.factor*rel[1])) + self._xrandr.configuration.outputs[self._draggingoutput].tentative_position = self._draggingsnap.suggest(newpos) + self._force_repaint() + + return True + + def _dragdrop_cb(self, widget, context, x, y, time): + if not self._draggingoutput: + return + + try: + self.set_position(self._draggingoutput, self._xrandr.configuration.outputs[self._draggingoutput].tentative_position) + except InadequateConfiguration: + context.finish(False, False, time) + #raise # snapping back to the original position should be enought feedback + + context.finish(True, False, time) + + def _dragend_cb(self, widget, context): + try: + del self._xrandr.configuration.outputs[self._draggingoutput].tentative_position + except KeyError: + pass # already reloaded + self._draggingoutput = None + self._draggingfrom = None + self._force_repaint() |