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