#!/usr/bin/python3
import gi, os, re, jwmkit_utils
import xml.etree.ElementTree as ET
from xml.dom import minidom
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GdkPixbuf

# JWM Kit - A set of Graphical Apps to simplify use of JWM (Joe's Window Manager) <https://codeberg.org/JWMKit/JWM_Kit>
# Copyright © 2020-2022 Calvin Kent McNabb <apps.jwmkit@gmail.com>
#
# This file is part of JWM Kit.
#
# JWM Kit is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2,
# as published by the Free Software Foundation.
#
# JWM Kit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with JWM Kit.  If not, see <https://www.gnu.org/licenses/>.


def get_path():
    home = os.path.expanduser('~')
    settings = home + '/.config/jwmkit/settings'
    path_ok = False
    if os.path.isfile(settings):
        with open(settings) as f:
            f = f.read()
        f = '\n{}'.format(f)
        try:
            path = re.findall('\nkeys.*=(.*)', f)[0]
            if path.startswith('$HOME'):
                path = home + path[5:]
            path_ok = True
        except IndexError:
            pass
    if not path_ok:
        return 'warning_settings'
    return path


class MainWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title="JWM Kit Keys")
        self.set_border_width(15)
        try:
            self.set_icon_from_file('/usr/share/pixmaps/jwmkit/keys.svg')
        except gi.repository.GLib.Error:
            self.set_icon_name('preferences-desktop-keyboard')
        self.path = get_path()
        if self.path == 'warning_settings':
            jwmkit_utils.warning_settings(self)
            return

        self.action_names = (
            'Desktop#', 'Move Up', 'Move Down', 'Move Right', 'Move Left', 'Stop/Exit', 'Selection', 'Task List Next',
            'Stack Next', 'Task List Previous', 'Stack Previous ', 'Close window', 'Toggle Fullscreen',
            'Minimize', 'Maximize', 'V Maximize', 'H Maximize', 'Max Top', 'Max Bottom', 'Max Left',
            'Max Right', 'Restore', 'Shade', 'Move', 'Resize', 'Show Menu', 'Desktop Right',
            'Desktop Left', 'Desktop Up', 'Desktop Down', 'Send Left', 'Send  Right', 'Send  Up',
            'Send Down', 'Show Trays', 'Restart JWM', 'Exit JWM')
        self.action_values = ('desktop#', 'up', 'down', 'right', 'left', 'escape', 'select', 'next', 'nextstacked',
                              'prev', 'prevstacked', 'close', 'fullscreen', 'minimize', 'maximize', 'maxv', 'maxh',
                              'maxtop', 'maxbottom', 'maxleft', 'maxright', 'restore', 'shade', 'move', 'resize',
                              'window', 'rdesktop', 'ldesktop', 'udesktop', 'ddesktop', 'sendl', 'sendr', 'sendu',
                              'sendd', 'showtray', 'restart', 'exit')
        self.mod_names = ('None', 'Alt', 'Ctrl', 'Shift', 'Mod 1', 'Mod 2', 'Mod 3', 'Mod 4', 'Mod 5')
        self.mod_values = ('', 'A', 'C', 'S', '1', '2', '3', '4', '5')
        self.context_values = ('border', 'close', 'icon', 'maximize', 'minimize', 'root', 'title')
        self.buttons = ['Button 1', 'Button 2', 'Button 3', 'Button 4', 'Button 5']

        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
        self.add(main_box)
        header_box = Gtk.Box(spacing=10)
        header_left = Gtk.Box(spacing=0)
        button_box = Gtk.Box(spacing=15)
        header_right = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
        header_box.add(header_left)
        header_right.add(Gtk.Label())
        header_right.add(button_box)
        header_box.pack_end(header_right, False, False, 15)
        main_box.add(header_box)
        header_label = Gtk.Label()
        header_label.set_markup('\n  <big><b>Keys</b></big>'
                                '\n  Keyboard &amp; Mouse Bindings\n')
        image = jwmkit_utils.create_image('/usr/share/pixmaps/jwmkit/keys.svg', 48, 48, True)
        header_left.pack_start(image, False, False, 10)
        header_left.add(header_label)

        report_button = Gtk.Button(label="Conflicts")
        report_button.set_tooltip_text('Check if your configuration conflicts with common shortcuts')
        report_button.connect("clicked", self.create_report)
        report_button.set_property("width-request", 100)

        self.about_button = Gtk.Button(label='About', image=Gtk.Image(stock=Gtk.STOCK_ABOUT))
        self.about_button.set_always_show_image(True)
        self.about_button.set_property("width-request", 100)
        self.about_button.set_no_show_all(True)
        self.about_button.set_visible(True)
        self.about_button.connect('clicked', jwmkit_utils.get_about, self)

        button_box.pack_end(self.about_button, False, False, 0)
        button_box.pack_end(report_button, False, False, 0)

        self.key_store = Gtk.ListStore(str, str, str, str, str, str)
        self.tree_view = Gtk.TreeView(model=self.key_store)
        self.tree_view.set_tooltip_text('Double click a row to edit')
        self.tree_view.connect("row-activated", self.edit_dialog, False)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn("Type\t", renderer_text, text=0)
        self.tree_view.append_column(column_text)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn("Key\t", renderer_text, text=1)
        self.tree_view.append_column(column_text)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn("Context\t", renderer_text, text=2)
        self.tree_view.append_column(column_text)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn("Mod Key", renderer_text, text=3)
        self.tree_view.append_column(column_text)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn("Mod Key", renderer_text, text=4)
        self.tree_view.append_column(column_text)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn("Action", renderer_text, text=5)
        self.tree_view.append_column(column_text)

        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        action_scroll = Gtk.ScrolledWindow()
        action_scroll.add(self.tree_view)
        action_scroll.set_min_content_height(340)
        action_scroll.set_min_content_width(640)
        row.pack_start(action_scroll, True, True, 10)
        main_box.pack_start(row, True, True, 0)

        add_button = Gtk.Button(label="Add", image=Gtk.Image(stock=Gtk.STOCK_ADD))
        remove_button = Gtk.Button(label="Remove", image=Gtk.Image(stock=Gtk.STOCK_DELETE))
        save_button = Gtk.Button(label="Save", image=Gtk.Image(stock=Gtk.STOCK_SAVE))
        close_button = Gtk.Button(label="Close", image=Gtk.Image(stock=Gtk.STOCK_CLOSE))

        add_button.connect("clicked", self.edit_dialog, -1, '', True)
        remove_button.connect("clicked", self.remove_command)
        save_button.connect("clicked", self.save_command)
        close_button.connect("clicked", Gtk.main_quit)

        add_button.set_always_show_image(True)
        remove_button.set_always_show_image(True)
        save_button.set_always_show_image(True)
        close_button.set_always_show_image(True)

        add_button.set_property("width-request", 100)
        remove_button.set_property("width-request", 100)
        save_button.set_property("width-request", 100)
        close_button.set_property("width-request", 100)

        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
        row.pack_start(add_button, False, False, 10)
        row.pack_start(remove_button, False, False, 0)
        row.pack_end(close_button, False, False, 10)
        row.pack_end(save_button, False, False, 0)
        main_box.pack_start(row, False, False, 0)
        self.read_data()

    def remove_command(self, button):
        selection = self.tree_view.get_selection()
        model, tree_iter = selection.get_selected_rows()
        i = tree_iter[0].get_indices()[0]
        dialog = Gtk.Dialog('Confirm', self, 0)
        dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        dialog.set_default_size(320, -1)
        box = dialog.get_content_area()
        box.set_border_width(10)
        box.add(Gtk.Label(label='\nRemove selected {} binding\n'.format(self.key_store[i][0]), xalign=.2))
        dialog.show_all()
        response = dialog.run()
        if response == -5:
            del self.key_store[i]
        dialog.destroy()

    def save_command(self, button):
        root = ET.Element('JWM')

        for data in self.key_store:
            if data[0] == 'Mouse':
                key = 'button'
            else:
                key = data[0].lower()
            element = ET.SubElement(root, data[0].replace('Keycode', 'Key'), {key: data[1].replace('Button ', '')})
            if data[0] == "Mouse":
                if data[2] not in (None, ''):
                    element.set('context', data[2])
            tmp = []
            if data[3] not in (None, ''):
                try:
                    tmp.append(self.mod_values[self.mod_names.index(data[3])])
                except (IndexError, ValueError):
                    pass
            if data[4] not in (None, '', data[3]):
                try:
                    tmp.append(self.mod_values[self.mod_names.index(data[4])])
                except (IndexError, ValueError):
                    pass
            if tmp:
                tmp = ''.join(tmp)
                element.set('mask', tmp)

            if data[5] not in (None, ''):
                if data[5] in self.action_names:
                    element.text = self.action_values[self.action_names.index(data[5])]
                else:
                    element.text = data[5]

        xml = b'\n'.join([s.strip() for s in ET.tostring(root).splitlines() if s.strip()])
        xml = minidom.parseString(xml).toprettyxml(newl='\n', indent='   ')
        xml = ET.ElementTree(ET.fromstring(xml))
        xml.write(self.path, encoding="utf-8", xml_declaration=True)

        dialog = Gtk.Dialog('Success!', self, 0)
        dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
        dialog.set_default_size(250, -1)
        mainbox = dialog.get_content_area()
        mainbox.set_spacing(10)
        mainbox.set_border_width(10)
        mainbox.add(Gtk.Label(label='\nSave complete\n'))
        dialog.show_all()
        dialog.run()
        dialog.destroy()
        os.system('jwm -restart')

    def read_data(self):
        try:
            tree = ET.parse(self.path)
            root = tree.getroot()
        except (ET.ParseError, FileNotFoundError):
            # data is missing from JWM XML file, or file is missing, or file is corrupt
            print('data is missing from JWM XML file, or file is missing, or file is corrupt\n')
            return []
        for node in root:
            tmp = []
            if node.tag == 'Mouse':
                tmp.append('Mouse')
                data = node.get('button')
                if not data: data = ''
                tmp.append(data)
                data = node.get('context')
                if not data: data = ''
                tmp.append(data.lower())
            elif node.tag == 'Key':
                tmp.append('Keycode') if 'keycode' in node.attrib else tmp.append('Key')
                data = node.get(tmp[0].lower())
                if not data: data = ''
                if len(data) == 1:
                    data = data.lower()
                tmp.append(data)
                tmp.append('')
            else:
                continue
            data = node.get('mask')
            if not data: data = ''
            try:
                tmp.append(self.mod_names[self.mod_values.index(data[0].upper())])
            except (ValueError, IndexError):
                tmp.append('')
            try:
                tmp.append(self.mod_names[self.mod_values.index(data[1].upper())])
            except (ValueError, IndexError):
                tmp.append('')
            data = node.text
            try:
                data = self.action_names[self.action_values.index(data.lower())]
            except(ValueError, IndexError):
                pass
            if not data: data = ''
            tmp.append(data)
            self.key_store.append(tmp)

    def edit_dialog(self, widget, path, column, new):
        def close_action(button):
            dialog.destroy()

        def check_toggle(check):
            if key_check.get_active():
                mouse_combo.set_visible(False)
                key_entry.set_visible(True)
                context_combo.set_sensitive(False)
            elif code_check.get_active():
                mouse_combo.set_visible(False)
                key_entry.set_visible(True)
                context_combo.set_sensitive(False)
            else:
                mouse_combo.set_visible(True)
                key_entry.set_visible(False)
                context_combo.set_sensitive(True)

        def get_key_name(entry, event):
            context_combo.set_sensitive(False)
            if code_check.get_active():
                entry.set_editable(True)
                return
            # convert input to key names recognized by jwm. Examples: Space, F1, F2, Escape, Backspace, a, b, c, 1, 2, 3
            # limit the entry to only one key name at a time
            entry.set_editable(False)
            key_value = Gdk.keyval_name(event.keyval)
            if key_value == 'numbersign':
                if action_combo.get_child().get_text() == 'Desktop#':
                    key_value = '#'
                else:
                    return
            if key_value.lower() in list('abcdefghijklmnopqrstuvwxyz0123456789'):
                entry.set_text(key_value.lower())
            elif key_value not in 'Shift_L, Shift_R, Control_L, Control_R, Alt_L, Alt_R, Super_L, Super_R, Meta_L, Meta_R':
                entry.set_text(key_value)
            else:
                entry.set_text('')
                key_value = ''

        def no_close(widget, event):
            # Prevent the escape key from closing the dialog
            return True

        def lockdesktop(combo):
            if action_combo.get_active() == 0:
                key_entry.set_sensitive(False)
                code_check.set_sensitive(False)
                mouse_check.set_sensitive(False)
            else:
                key_entry.set_sensitive(True)
                code_check.set_sensitive(True)
                mouse_check.set_sensitive(True)

        def ok_action(button):
            data = []
            if code_check.get_active():
                data.append('Keycode')
                key = key_entry.get_text()
            elif mouse_check.get_active():
                data.append('Mouse')
                key = str(mouse_combo.get_active() + 1)
            else:
                data.append('Key')
                key = key_entry.get_text()
            data.append(key)

            i = context_combo.get_active()
            if i != -1:
                data.append(self.context_values[i])
            else:
                data.append('')
            i = mod1_combo.get_active()
            if i not in (-1, 0):
                data.append(self.mod_names[i])
            else:
                data.append('')
            ii = mod2_combo.get_active()
            if ii not in (-1, 0, i):
                data.append(self.mod_names[ii])
            else:
                data.append('')
            action = action_combo.get_child().get_text()
            if action not in self.action_names:
                if action[:5] not in('root:', 'exec:', ''):
                    action = 'exec:{}'.format(action)
            data.append(action)
            if not new:
                self.key_store[path] = data
            else:
                self.key_store.append(data)
            dialog.destroy()
            if new:
                self.tree_view.set_cursor(len(self.key_store) - 1)

        if new:
            data = ['', '', '', '', '', '']
        else:
            data = self.key_store[path]
        dialog = Gtk.Dialog('Configure Binding', self, 0)
        dialog.connect("delete-event", no_close)
        mainbox = dialog.get_content_area()
        mainbox.set_spacing(15)
        mainbox.set_border_width(20)
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
        mainbox.add(vbox)
        vbox.add(row)
        label = Gtk.Label()
        label.set_markup("\n<big><b>JWM Kit Easy Keys</b></big>\nConfigure a Key / Mouse Binding\n")
        row.add(label)

        key_entry = Gtk.Entry()
        key_entry.set_no_show_all(True)
        key_entry.set_width_chars(10)
        key_entry.connect("key-press-event", get_key_name)

        mouse_combo = Gtk.ComboBoxText()
        mouse_combo.set_no_show_all(True)
        mouse_combo.set_property("width-request", 100)
        key_entry.set_property("width-request", 100)

        context_combo = Gtk.ComboBoxText()
        mod1_combo = Gtk.ComboBoxText()
        mod2_combo = Gtk.ComboBoxText()
        action_combo = Gtk.ComboBoxText.new_with_entry()
        action_combo.connect("changed", lockdesktop)

        key_check = Gtk.RadioButton.new_with_label_from_widget(None, "Key")
        key_check.connect("toggled", check_toggle)

        code_check = Gtk.RadioButton.new_with_mnemonic_from_widget(key_check, "Keycode")
        code_check.connect("toggled", check_toggle)

        mouse_check = Gtk.RadioButton.new_with_mnemonic_from_widget(key_check, "Mouse")
        mouse_check.connect("toggled", check_toggle)

        for i in self.buttons:
            mouse_combo.append_text(i)

        for i in self.context_values:
            context_combo.append_text(i)

        for i in self.mod_names:
            mod1_combo.append_text(i)
            mod2_combo.append_text(i)

        for i in self.action_names:
            action_combo.append_text(i)

        grid = Gtk.Grid(column_homogeneous=False, column_spacing=10, row_spacing=10)
        key_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
        vbox.add(grid)

        grid.attach(Gtk.Label(label='Key / Mouse'), 0, 0, 1, 1)
        grid.attach(Gtk.Label(label='Context'), 1, 0, 1, 1)
        grid.attach(Gtk.Label(label='Mod Key'), 2, 0, 1, 1)
        grid.attach(Gtk.Label(label='Mod Key'), 3, 0, 1, 1)
        grid.attach(Gtk.Label(label='Action'), 4, 0, 1, 1)

        key_box.add(key_entry)
        key_box.add(mouse_combo)
        grid.attach(key_box, 0, 1, 1, 1)
        grid.attach(context_combo, 1, 1, 1, 1)
        grid.attach(mod1_combo, 2, 1, 1, 1)
        grid.attach(mod2_combo, 3, 1, 1, 1)
        grid.attach(action_combo, 4, 1, 1, 1)

        grid.attach(key_check, 0, 2, 1, 1)
        grid.attach(code_check, 0, 3, 1, 1)
        grid.attach(mouse_check, 0, 4, 1, 1)

        cancel_button = Gtk.Button(label='Cancel', image=Gtk.Image(stock=Gtk.STOCK_CANCEL))
        cancel_button.set_property("width-request", 100)
        cancel_button.set_always_show_image(True)
        cancel_button.connect("clicked", close_action)

        ok_button = Gtk.Button(label='OK', image=Gtk.Image(stock=Gtk.STOCK_OK))
        ok_button.set_property("width-request", 100)
        ok_button.set_always_show_image(True)
        ok_button.connect("clicked", ok_action)

        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
        row.pack_end(cancel_button, False, False, 0)
        row.pack_end(ok_button, False, False, 0)
        vbox.add(row)
        if data[0] == 'Mouse':
            mouse_check.set_active(True)
            mouse_combo.set_visible(True)
            try:
                mouse_combo.set_active(int(data[1][-1]) - 1)
            except (ValueError, IndexError):
                pass
            try:
                context_combo.set_active(self.context_values.index(data[2]))
            except (ValueError, IndexError):
                pass
        else:
            key_entry.set_visible(True)
            if data[0] == 'Keycode':
                code_check.set_active(True)
            key_entry.set_text(data[1])
            context_combo.set_sensitive(False)
        if data[3] in self.mod_names:
            mod1_combo.set_active(self.mod_names.index(data[3]))
        if data[4] in self.mod_names:
            mod2_combo.set_active(self.mod_names.index(data[4]))
        if data[5] in self.action_names:
            i = self.action_names.index(data[5])
            action_combo.set_active(i)
            if i == 0:
                key_entry.set_sensitive(False)
        else:
            action_combo.get_child().set_text(data[5])

        dialog.show_all()

    def create_report(self, button):
        tty_keys = (('Ctrl', 'Alt', 'F1'), ('Ctrl', 'Alt', 'F2'), ('Ctrl', 'Alt', 'F3'), ('Ctrl', 'Alt', 'F4'),
                    ('Ctrl', 'Alt', 'F5'), ('Ctrl', 'Alt', 'F6'), ('Ctrl', 'Alt', 'F7'))
        common_keys = (('Ctrl', '', 'c'), ('Ctrl', '', 'Insert'), ('Ctrl', '', 'v'), ('Ctrl', '', 'x'),
                       ('Ctrl', '', 'z'), ('Ctrl', '', 'y'), ('Ctrl', '', 'a'), ('Ctrl', '', 'o'), ('Ctrl', '', 's'),
                       ('Ctrl', '', 'f'), ('Ctrl', '', 'g'), ('Ctrl', '', 'p'), ('Ctrl', '', 'Tab'))
        common_names = ('Copy', 'Copy', 'Paste', 'Cut', 'Undo', 'Redo', 'Select All', 'Open',
                        'Save', 'Find', 'Find Next', 'Print', 'Next Tab')
        tty_report = []
        common_report = []
        for shortcut in self.key_store:
            shortcut, rev_shortcut = (shortcut[3], shortcut[4], shortcut[1]), (shortcut[4], shortcut[3], shortcut[1])
            if shortcut in tty_keys or rev_shortcut in tty_keys:
                tmp = ''
                for s in shortcut:
                    if s != '':
                        tmp = '{}{}--'.format(tmp, s.upper())
                tty_report.append(tmp[:-2])
            if shortcut in common_keys or rev_shortcut in common_keys:
                tmp = ''
                try:
                    c = common_names[common_keys.index(shortcut)]
                except ValueError:
                    c = common_names[common_keys.index(rev_shortcut)]
                for s in shortcut:
                    if s != '':
                        tmp = '{}{}--'.format(tmp, s.upper())
                common_report.append((tmp[:-2], c))

        dialog = Gtk.Dialog('Conflicts', self, 0)
        dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
        dialog.set_default_size(450, -1)
        mainbox = dialog.get_content_area()
        mainbox.set_spacing(15)
        mainbox.set_border_width(25)
        report_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
        scroll = Gtk.ScrolledWindow()
        scroll.set_min_content_height(180)
        scroll.add(report_box)
        label = Gtk.Label()
        label.set_markup("<big><b>Conflict Report</b></big>")
        mainbox.add(label)
        label = Gtk.Label(label='Common shortcuts that may conflict with your configuration.', xalign=0)
        mainbox.add(label)
        mainbox.add(scroll)

        for conflict in tty_report:
            report_box.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
            row = Gtk.Box(spacing=4)
            report_box.add(row)
            row.pack_start(Gtk.Label(label='{}\t\t'.format(conflict)), False, False, 0)
            row.pack_end(Gtk.Label(label='TTY shortcuts'), False, False, 0)

        for conflict in common_report:
            report_box.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
            row = Gtk.Box(spacing=4)
            report_box.add(row)
            row.pack_start(Gtk.Label(label='{}\t\t'.format(conflict[0])), False, False, 0)
            row.pack_end(Gtk.Label(label='Common shortcuts for {}'.format(conflict[1])), False, False, 0)
        if len(tty_report) == 0 and len(common_report) == 0:
            report_box.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
            row = Gtk.Box(spacing=4)
            report_box.add(row)
            label = Gtk.Label()
            label.set_markup("\n<i><b>Good JOB!\tNo conflicts found</b></i>")
            row.pack_end(label, True, True, 0)

        mainbox.add(Gtk.Label())
        dialog.show_all()
        dialog.run()
        dialog.destroy()


win = MainWindow()
win.connect("delete-event", Gtk.main_quit)
win.set_position(Gtk.WindowPosition.CENTER)
win.show_all()
Gtk.main()
