1# ARandR -- Another XRandR GUI 2# Copyright (C) 2008 -- 2011 chrysn <chrysn@fsfe.org> 3# copyright (C) 2019 actionless <actionless.loveless@gmail.com> 4# 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <http://www.gnu.org/licenses/>. 17"""Main GUI for ARandR""" 18# pylint: disable=deprecated-method,deprecated-module,wrong-import-order,missing-docstring,wrong-import-position 19 20import os 21import optparse 22import inspect 23 24# import os 25# os.environ['DISPLAY']=':0.0' 26 27import gi 28gi.require_version('Gtk', '3.0') 29from gi.repository import Gtk 30 31from . import widget 32from .i18n import _ 33from .meta import ( 34 __version__, TRANSLATORS, COPYRIGHT, PROGRAMNAME, PROGRAMDESCRIPTION, 35) 36 37 38def actioncallback(function): 39 """Wrapper around a function that is intended to be used both as a callback 40 from a Gtk.Action and as a normal function. 41 42 Functions taking no arguments will never be given any, functions taking one 43 argument (callbacks for radio actions) will be given the value of the action 44 or just the argument. 45 46 A first argument called 'self' is passed through. 47 """ 48 argnames = inspect.getargspec(function)[0] 49 if argnames[0] == 'self': 50 has_self = True 51 argnames.pop(0) 52 else: 53 has_self = False 54 assert len(argnames) in (0, 1) 55 56 def wrapper(*args): 57 args_in = list(args) 58 args_out = [] 59 if has_self: 60 args_out.append(args_in.pop(0)) 61 if len(argnames) == len(args_in): # called directly 62 args_out.extend(args_in) 63 elif len(argnames) + 1 == len(args_in): 64 if argnames: 65 args_out.append(args_in[1].props.value) 66 else: 67 raise TypeError("Arguments don't match") 68 69 return function(*args_out) 70 71 wrapper.__name__ = function.__name__ 72 wrapper.__doc__ = function.__doc__ 73 return wrapper 74 75 76class Application: 77 uixml = """ 78 <ui> 79 <menubar name="MenuBar"> 80 <menu action="Layout"> 81 <menuitem action="New" /> 82 <menuitem action="Open" /> 83 <menuitem action="SaveAs" /> 84 <separator /> 85 <menuitem action="Apply" /> 86 <menuitem action="LayoutSettings" /> 87 <separator /> 88 <menuitem action="Quit" /> 89 </menu> 90 <menu action="View"> 91 <menuitem action="Zoom4" /> 92 <menuitem action="Zoom8" /> 93 <menuitem action="Zoom16" /> 94 </menu> 95 <menu action="Outputs" name="Outputs"> 96 <menuitem action="OutputsDummy" /> 97 </menu> 98 <menu action="Help"> 99 <menuitem action="About" /> 100 </menu> 101 </menubar> 102 <toolbar name="ToolBar"> 103 <toolitem action="Apply" /> 104 <separator /> 105 <toolitem action="New" /> 106 <toolitem action="Open" /> 107 <toolitem action="SaveAs" /> 108 </toolbar> 109 </ui> 110 """ 111 112 def __init__(self, file=None, randr_display=None, force_version=False): 113 self.window = window = Gtk.Window() 114 window.props.title = "Screen Layout Editor" 115 116 # actions 117 actiongroup = Gtk.ActionGroup('default') 118 actiongroup.add_actions([ 119 ("Layout", None, _("_Layout")), 120 ("New", Gtk.STOCK_NEW, None, None, None, self.do_new), 121 ("Open", Gtk.STOCK_OPEN, None, None, None, self.do_open), 122 ("SaveAs", Gtk.STOCK_SAVE_AS, None, None, None, self.do_save_as), 123 124 ("Apply", Gtk.STOCK_APPLY, None, '<Control>Return', None, self.do_apply), 125 ("LayoutSettings", Gtk.STOCK_PROPERTIES, None, 126 '<Alt>Return', None, self.do_open_properties), 127 128 ("Quit", Gtk.STOCK_QUIT, None, None, None, Gtk.main_quit), 129 130 131 ("View", None, _("_View")), 132 133 ("Outputs", None, _("_Outputs")), 134 ("OutputsDummy", None, _("Dummy")), 135 136 ("Help", None, _("_Help")), 137 ("About", Gtk.STOCK_ABOUT, None, None, None, self.about), 138 ]) 139 actiongroup.add_radio_actions([ 140 ("Zoom4", None, _("1:4"), None, None, 4), 141 ("Zoom8", None, _("1:8"), None, None, 8), 142 ("Zoom16", None, _("1:16"), None, None, 16), 143 ], 8, self.set_zoom) 144 145 window.connect('destroy', Gtk.main_quit) 146 147 # uimanager 148 self.uimanager = Gtk.UIManager() 149 accelgroup = self.uimanager.get_accel_group() 150 window.add_accel_group(accelgroup) 151 152 self.uimanager.insert_action_group(actiongroup, 0) 153 154 self.uimanager.add_ui_from_string(self.uixml) 155 156 # widget 157 self.widget = widget.ARandRWidget( 158 display=randr_display, force_version=force_version, 159 window=self.window 160 ) 161 if file is None: 162 self.filetemplate = self.widget.load_from_x() 163 else: 164 self.filetemplate = self.widget.load_from_file(file) 165 166 self.widget.connect('changed', self._widget_changed) 167 self._widget_changed(self.widget) 168 169 # window layout 170 vbox = Gtk.VBox() 171 menubar = self.uimanager.get_widget('/MenuBar') 172 vbox.pack_start(menubar, expand=False, fill=False, padding=0) 173 toolbar = self.uimanager.get_widget('/ToolBar') 174 vbox.pack_start(toolbar, expand=False, fill=False, padding=0) 175 176 vbox.add(self.widget) 177 178 window.add(vbox) 179 window.show_all() 180 181 self.gconf = None 182 183 #################### actions #################### 184 185 @actioncallback 186 # don't use directly: state is not pushed back to action group. 187 def set_zoom(self, value): 188 self.widget.factor = value 189 #self.window.resize(1, 1) 190 191 @actioncallback 192 def do_open_properties(self): 193 dialog = Gtk.Dialog( 194 _("Script Properties"), None, 195 Gtk.DialogFlags.MODAL, (Gtk.STOCK_CLOSE, Gtk.ResponseType.ACCEPT) 196 ) 197 dialog.set_default_size(300, 400) 198 199 script_editor = Gtk.TextView() 200 script_buffer = script_editor.get_buffer() 201 script_buffer.set_text("\n".join(self.filetemplate)) 202 script_editor.props.editable = False 203 204 # wacom_options = Gtk.Label("FIXME") 205 206 notebook = Gtk.Notebook() 207 # notebook.append_page(wacom_options, Gtk.Label(_("Wacom options"))) 208 notebook.append_page(script_editor, Gtk.Label(_("Script"))) 209 210 dialog.vbox.pack_start(notebook, expand=False, fill=False, padding=0) # pylint: disable=no-member 211 dialog.show_all() 212 213 dialog.run() 214 dialog.destroy() 215 216 @actioncallback 217 def do_apply(self): 218 if self.widget.abort_if_unsafe(): 219 return 220 221 try: 222 self.widget.save_to_x() 223 except Exception as exc: # pylint: disable=broad-except 224 dialog = Gtk.MessageDialog( 225 None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, 226 Gtk.ButtonsType.OK, _("XRandR failed:\n%s") % exc 227 ) 228 dialog.run() 229 dialog.destroy() 230 231 @actioncallback 232 def do_new(self): 233 self.filetemplate = self.widget.load_from_x() 234 235 @actioncallback 236 def do_open(self): 237 dialog = self._new_file_dialog( 238 _("Open Layout"), Gtk.FileChooserAction.OPEN, Gtk.STOCK_OPEN 239 ) 240 241 result = dialog.run() 242 filenames = dialog.get_filenames() 243 dialog.destroy() 244 if result == Gtk.ResponseType.ACCEPT: 245 assert len(filenames) == 1 246 filename = filenames[0] 247 self.filetemplate = self.widget.load_from_file(filename) 248 249 @actioncallback 250 def do_save_as(self): 251 dialog = self._new_file_dialog( 252 _("Save Layout"), Gtk.FileChooserAction.SAVE, Gtk.STOCK_SAVE 253 ) 254 dialog.props.do_overwrite_confirmation = True 255 256 result = dialog.run() 257 filenames = dialog.get_filenames() 258 dialog.destroy() 259 if result == Gtk.ResponseType.ACCEPT: 260 assert len(filenames) == 1 261 filename = filenames[0] 262 if not filename.endswith('.sh'): 263 filename = filename + '.sh' 264 self.widget.save_to_file(filename, self.filetemplate) 265 266 def _new_file_dialog(self, title, dialog_type, buttontype): # pylint: disable=no-self-use 267 dialog = Gtk.FileChooserDialog(title, None, dialog_type) 268 dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) 269 dialog.add_button(buttontype, Gtk.ResponseType.ACCEPT) 270 271 layoutdir = os.path.expanduser('~/.screenlayout/') 272 try: 273 os.makedirs(layoutdir) 274 except OSError: 275 pass 276 dialog.set_current_folder(layoutdir) 277 278 file_filter = Gtk.FileFilter() 279 file_filter.set_name('Shell script (Layout file)') 280 file_filter.add_pattern('*.sh') 281 dialog.add_filter(file_filter) 282 283 return dialog 284 285 #################### widget maintenance #################### 286 287 def _widget_changed(self, _widget): 288 self._populate_outputs() 289 290 def _populate_outputs(self): 291 outputs_widget = self.uimanager.get_widget('/MenuBar/Outputs') 292 outputs_widget.props.submenu = self.widget.contextmenu() 293 294 #################### application related #################### 295 296 def about(self, *_args): # pylint: disable=no-self-use 297 dialog = Gtk.AboutDialog() 298 dialog.props.program_name = PROGRAMNAME 299 dialog.props.version = __version__ 300 dialog.props.translator_credits = "\n".join(TRANSLATORS) 301 dialog.props.copyright = COPYRIGHT 302 dialog.props.comments = PROGRAMDESCRIPTION 303 licensetext = open(os.path.join(os.path.dirname( 304 __file__), 'data', 'gpl-3.txt')).read() 305 dialog.props.license = licensetext.replace( 306 '<', u'\u2329 ').replace('>', u' \u232a') 307 dialog.props.logo_icon_name = 'video-display' 308 dialog.run() 309 dialog.destroy() 310 311 def run(self): # pylint: disable=no-self-use 312 Gtk.main() 313 314 315def main(): 316 parser = optparse.OptionParser( 317 usage="%prog [savedfile]", 318 description="Another XRandrR GUI", 319 version="%%prog %s" % __version__ 320 ) 321 parser.add_option( 322 '--randr-display', 323 help=( 324 'Use D as display for xrandr ' 325 '(but still show the GUI on the display from the environment; ' 326 'e.g. `localhost:10.0`)' 327 ), 328 metavar='D' 329 ) 330 parser.add_option( 331 '--force-version', 332 help='Even run with untested XRandR versions', 333 action='store_true' 334 ) 335 336 (options, args) = parser.parse_args() 337 if not args: 338 file_to_open = None 339 elif len(args) == 1: 340 file_to_open = args[0] 341 else: 342 parser.usage() 343 344 app = Application( 345 file=file_to_open, 346 randr_display=options.randr_display, 347 force_version=options.force_version 348 ) 349 app.run() 350