# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025 Eduardo Aguiar
#
# 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, or (at your option) any later
# version.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
import json
import os
import os.path

import mobius
import pymobius
from gi.repository import GLib
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import Gtk

from metadata import *

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Windows constants
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
(CATEGORY_ICON, CATEGORY_NAME, CATEGORY_OBJ) = range(3)
(ATTR_ID, ATTR_NAME, ATTR_VALUE_MASK) = range(3)


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief resize icon data
# @param icon_data icon data
# @return new icon data
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def resize_icon_data(icon_data):
    loader = GdkPixbuf.PixbufLoader()
    loader.set_size(64, 64)
    loader.write(icon_data)
    loader.close()

    pixbuf = loader.get_pixbuf()
    result, data = pixbuf.save_to_bufferv("png", [None], [])

    if result:
        return data

    return icon_data


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Import category database from .json file
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def import_json(f):
    # read file
    fp = f.new_reader()
    categories = json.load(fp)

    # insert data
    transaction = mobius.framework.new_category_transaction()
    mobius.framework.remove_categories()

    for d_category in categories:
        category = mobius.framework.new_category(d_category.get('id'))
        category.name = d_category.get('name')
        category.description = d_category.get('description')
        category.icon_data = mobius.decoder.base64(d_category.get('icon'))

        # attributes
        for d_attr in d_category.get('attributes'):
            attr = category.new_attribute(d_attr.get('id'))
            attr.name = d_attr.get('name')
            attr.description = d_attr.get('description')
            attr.datatype = d_attr.get('datatype')
            attr.value_mask = d_attr.get('value_mask')

    transaction.commit()


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Export category database as .json file
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def export_json(f):
    # generate data
    categories = []

    for category in mobius.framework.get_categories():
        d_category = {
            'id': category.id,
            'name': category.name,
            'description': category.description,
            'icon': mobius.encoder.base64(category.icon_data)
        }

        attrlist = []
        for attr in category.get_attributes():
            d_attr = {
                'id': attr.id,
                'name': attr.name,
                'description': attr.description,
                'datatype': attr.datatype,
                'value_mask': attr.value_mask,
                'index': attr.index
            }
            attrlist.append(d_attr)

        d_category['attributes'] = attrlist
        categories.append(d_category)

    # write file
    fp = mobius.io.text_writer(f.new_writer())
    json.dump(categories, fp)


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Widget
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Widget(object):

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Initialize widget
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __init__(self):
        self.__widget = mobius.ui.box(mobius.ui.box.orientation_vertical)
        self.__mediator = pymobius.mediator.copy()
        self.__widget.set_border_width(5)
        self.__widget.set_spacing(5)
        self.__widget.set_visible(True)

        # menubar
        menubar = Gtk.MenuBar()
        menubar.show()
        self.__widget.add_child(menubar, mobius.ui.box.fill_none)

        item = Gtk.MenuItem.new_with_mnemonic('_File')
        item.show()
        menubar.append(item)

        menu = Gtk.Menu()
        menu.show()
        item.set_submenu(menu)

        item = Gtk.MenuItem.new_with_mnemonic('_Export...')
        item.connect("activate", self.on_model_export)
        item.show()
        menu.append(item)

        item = Gtk.MenuItem.new_with_mnemonic('_Import...')
        item.connect("activate", self.on_model_import)
        item.show()
        menu.append(item)

        item = Gtk.SeparatorMenuItem.new()
        item.show()
        menu.append(item)

        item = Gtk.MenuItem.new_with_mnemonic('_Quit')
        item.connect("activate", self.on_extension_close)
        item.show()
        menu.append(item)

        # hpaned
        hpaned = Gtk.HPaned()
        hpaned.show()
        self.__widget.add_child(hpaned, mobius.ui.box.fill_with_widget)

        # category listview
        vbox1 = mobius.ui.box(mobius.ui.box.orientation_vertical)
        vbox1.set_spacing(5)
        vbox1.set_visible(True)
        hpaned.pack1(vbox1.get_ui_widget(), True, True)

        frame = Gtk.Frame()
        frame.show()
        vbox1.add_child(frame, mobius.ui.box.fill_with_widget)

        sw = Gtk.ScrolledWindow()
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        sw.show()
        frame.add(sw)

        model = Gtk.ListStore.new([GdkPixbuf.Pixbuf, str, object])
        model.set_sort_column_id(CATEGORY_NAME, Gtk.SortType.ASCENDING)

        self.category_listview = Gtk.TreeView.new_with_model(model)
        self.category_listview.set_headers_visible(False)
        self.category_listview.show()
        sw.add(self.category_listview)

        selection = self.category_listview.get_selection()
        selection.connect('changed', self.on_category_selection_changed)

        renderer = Gtk.CellRendererPixbuf()
        tvcolumn = Gtk.TreeViewColumn()
        tvcolumn.pack_start(renderer, True)
        tvcolumn.add_attribute(renderer, 'pixbuf', CATEGORY_ICON)
        self.category_listview.append_column(tvcolumn)

        renderer = Gtk.CellRendererText()
        tvcolumn = Gtk.TreeViewColumn()
        tvcolumn.pack_start(renderer, True)
        tvcolumn.add_attribute(renderer, 'text', CATEGORY_NAME)
        self.category_listview.append_column(tvcolumn)

        # buttons
        hbox = mobius.ui.box(mobius.ui.box.orientation_horizontal)
        hbox.set_visible(True)
        vbox1.add_child(hbox, mobius.ui.box.fill_none)

        button = mobius.ui.button()
        button.set_icon_by_name('list-add')
        button.set_text("_Add")
        button.set_visible(True)
        button.set_callback('clicked', self.__on_category_add)
        hbox.add_child(button, mobius.ui.box.fill_none)

        hbox.add_filler()

        self.__remove_button = mobius.ui.button()
        self.__remove_button.set_icon_by_name('list-remove')
        self.__remove_button.set_text("_Remove")
        self.__remove_button.set_sensitive(False)
        self.__remove_button.set_visible(True)
        self.__remove_button.set_callback('clicked', self.__on_category_remove)
        hbox.add_child(self.__remove_button, mobius.ui.box.fill_none)

        # notebook
        notebook = Gtk.Notebook()
        notebook.show()
        hpaned.pack2(notebook, True, True)

        # page: general
        vbox2 = mobius.ui.box(mobius.ui.box.orientation_vertical)
        vbox2.set_border_width(5)
        vbox2.set_visible(True)
        notebook.append_page(vbox2.get_ui_widget(), mobius.ui.label('General').get_ui_widget())

        grid = Gtk.Grid.new()
        grid.set_row_spacing(10)
        grid.set_column_spacing(5)
        grid.show()
        vbox2.add_child(grid, mobius.ui.box.fill_with_widget)

        label = mobius.ui.label()
        label.set_markup('<b>ID</b>')
        label.set_halign(mobius.ui.label.align_right)
        label.set_visible(True)
        grid.attach(label.get_ui_widget(), 0, 0, 1, 1)

        self.category_id_entry = Gtk.Entry()
        self.category_id_entry.set_editable(False)
        self.category_id_entry.set_sensitive(False)
        self.category_id_entry.set_hexpand(True)
        self.category_id_entry.show()
        grid.attach(self.category_id_entry, 1, 0, 2, 1)

        label = mobius.ui.label()
        label.set_markup('<b>Name</b>')
        label.set_halign(mobius.ui.label.align_right)
        label.set_visible(True)
        grid.attach(label.get_ui_widget(), 0, 1, 1, 1)

        self.category_name_entry = Gtk.Entry()
        self.category_name_entry.connect('changed', self.on_category_name_changed)
        self.category_name_entry.show()
        grid.attach(self.category_name_entry, 1, 1, 2, 1)

        label = mobius.ui.label()
        label.set_markup('<b>Icon</b>')
        label.set_halign(mobius.ui.label.align_right)
        label.set_visible(True)
        grid.attach(label.get_ui_widget(), 0, 2, 1, 1)

        icon = mobius.ui.new_icon_by_name('image-missing', mobius.ui.icon.size_dnd)
        self.__category_icon_button = mobius.ui.button()
        self.__category_icon_button.set_icon(icon)
        self.__category_icon_button.set_visible(True)
        self.__category_icon_button.set_callback('clicked', self.__on_category_icon_edit)

        grid.attach(self.__category_icon_button.get_ui_widget(), 1, 2, 1, 1)

        placeholder = mobius.ui.label(' ')
        placeholder.set_visible(True)
        grid.attach(placeholder.get_ui_widget(), 2, 0, 1, 1)

        # page: attributes
        vbox_page = mobius.ui.box(mobius.ui.box.orientation_vertical)
        vbox_page.set_spacing(5)
        vbox_page.set_visible(True)
        notebook.append_page(vbox_page.get_ui_widget(), mobius.ui.label('Attributes').get_ui_widget())

        sw = Gtk.ScrolledWindow()
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        sw.show()
        vbox_page.add_child(sw, mobius.ui.box.fill_with_widget)

        # attributes listview
        datastore = Gtk.ListStore.new([str, str, str])
        self.attribute_listview = Gtk.TreeView.new_with_model(datastore)
        self.attribute_listview.set_enable_search(False)

        selection = self.attribute_listview.get_selection()
        selection.connect('changed', self.on_attribute_selection_changed)

        renderer = Gtk.CellRendererText()
        renderer.set_property('editable', True)
        renderer.connect('edited', self.on_attribute_id_edited)

        tvcolumn = Gtk.TreeViewColumn('ID')
        tvcolumn.pack_start(renderer, True)
        tvcolumn.add_attribute(renderer, 'text', ATTR_ID)
        tvcolumn.set_resizable(True)
        self.attribute_listview.append_column(tvcolumn)

        renderer = Gtk.CellRendererText()
        renderer.set_property('editable', True)
        renderer.connect('edited', self.on_attribute_name_edited)

        tvcolumn = Gtk.TreeViewColumn('Name')
        tvcolumn.pack_start(renderer, True)
        tvcolumn.add_attribute(renderer, 'text', ATTR_NAME)
        self.attribute_listview.append_column(tvcolumn)
        self.attribute_listview.show()
        sw.add(self.attribute_listview)

        # value_mask
        hbox = mobius.ui.box(mobius.ui.box.orientation_horizontal)
        hbox.set_spacing(5)
        hbox.set_visible(True)
        vbox_page.add_child(hbox, mobius.ui.box.fill_none)

        label = mobius.ui.label()
        label.set_markup('<b>Value mask</b>')
        label.set_visible(True)
        hbox.add_child(label, mobius.ui.box.fill_none)

        self.value_mask_entry = Gtk.Entry()
        self.value_mask_entry.connect('changed', self.on_value_mask_changed)
        self.value_mask_entry.show()
        hbox.add_child(self.value_mask_entry, mobius.ui.box.fill_with_widget)

        # attribute buttons
        hbox = mobius.ui.box(mobius.ui.box.orientation_horizontal)
        hbox.set_visible(True)
        vbox_page.add_child(hbox, mobius.ui.box.fill_none)

        self.__add_attr_button = mobius.ui.button()
        self.__add_attr_button.set_icon_by_name('list-add')
        self.__add_attr_button.set_visible(True)
        self.__add_attr_button.set_callback('clicked', self.__on_attribute_add)
        hbox.add_child(self.__add_attr_button, mobius.ui.box.fill_none)

        self.__remove_attr_button = mobius.ui.button()
        self.__remove_attr_button.set_icon_by_name('list-remove')
        self.__remove_attr_button.set_sensitive(False)
        self.__remove_attr_button.set_visible(True)
        self.__remove_attr_button.set_callback('clicked', self.__on_attribute_remove)
        hbox.add_child(self.__remove_attr_button, mobius.ui.box.fill_none)

        self.__up_attr_button = mobius.ui.button()
        self.__up_attr_button.set_icon_by_name('go-up')
        self.__up_attr_button.set_sensitive(False)
        self.__up_attr_button.set_visible(True)
        self.__up_attr_button.set_callback('clicked', self.__on_attribute_up)
        hbox.add_child(self.__up_attr_button, mobius.ui.box.fill_none)

        self.__down_attr_button = mobius.ui.button()
        self.__down_attr_button.set_icon_by_name('go-down')
        self.__down_attr_button.set_sensitive(False)
        self.__down_attr_button.set_visible(True)
        self.__down_attr_button.set_callback('clicked', self.__on_attribute_down)
        hbox.add_child(self.__down_attr_button, mobius.ui.box.fill_none)

        # flags
        self.is_selecting = False  # selecting new category
        self.is_setting_value_mask = False  # setting value_mask text
        self.category = None  # current selected category

        self.populate_category_listview()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Get internal widget
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def get_widget(self):
        return self.__widget

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Populate category listview
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def populate_category_listview(self):
        model = self.category_listview.get_model()
        model.clear()

        for category in mobius.framework.get_categories():
            icon = self.__render_icon(category)
            model.append((icon, category.name, category))

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Populate attribute listview
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def populate_attribute_listview(self, category):
        model = self.attribute_listview.get_model()
        model.clear()

        for a in category.get_attributes():
            if a.name and a.name[0].islower():
                name = a.name.capitalize()
            else:
                name = a.name
            model.append((a.id, name, a.value_mask))

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle close button
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_extension_close(self, widget, *args):
        self.__mediator.call('ui.working-area.close', self.working_area.id)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief show save/ignore/cancel dialog if there are modified items
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_widget_stopped(self):  # test
        self.__mediator.clear()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle model-export event
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_model_export(self, widget, *args):

        # choose file
        fs = Gtk.FileChooserDialog(title='Export category database', action=Gtk.FileChooserAction.SAVE)
        fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        fs.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
        fs.set_do_overwrite_confirmation(True)
        fs.set_current_name('category.json')

        fc_filter = Gtk.FileFilter()
        fc_filter.set_name('Category Manager')
        fc_filter.add_pattern('*.json')
        fs.set_filter(fc_filter)

        rc = fs.run()
        uri = fs.get_uri()
        fs.destroy()

        if rc != Gtk.ResponseType.OK:
            return

        root, ext = os.path.splitext(uri)
        if ext != '.json':
            uri += '.json'

        # export database
        f = mobius.io.new_file_by_url(uri)
        export_json(f)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle model-import event
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_model_import(self, widget, *args):

        # build filechooser
        fs = Gtk.FileChooserDialog(title='Import category database')
        fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        fs.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)

        fc_filter = Gtk.FileFilter()
        fc_filter.add_pattern('*.json')
        fs.set_filter(fc_filter)

        # run dialog
        rc = fs.run()
        uri = fs.get_uri()
        fs.destroy()

        if rc != Gtk.ResponseType.OK:
            return

        # import database
        f = mobius.io.new_file_by_url(uri)
        import_json(f)
        self.populate_category_listview()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle category selection
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_category_selection_changed(self, selection, *args):
        model, treeiter = selection.get_selected()
        self.is_selecting = True

        # fill info
        if treeiter:
            category = model.get_value(treeiter, CATEGORY_OBJ)

            self.category_id_entry.set_text(category.id)
            self.category_name_entry.set_text(category.name or '')

            icon = mobius.ui.new_icon_from_data(category.icon_data, mobius.ui.icon.size_dnd)
            self.__category_icon_button.set_icon(icon)

            self.populate_attribute_listview(category)
            self.category = category

        else:
            self.category_id_entry.set_text('')
            self.category_name_entry.set_text('')
            icon = mobius.ui.new_icon_by_name('image-missing', mobius.ui.icon.size_dnd)
            self.__category_icon_button.set_icon(icon)

            attr_model = self.attribute_listview.get_model()
            attr_model.clear()
            self.category = None

        self.is_selecting = False

        # enable/disable widgets
        enabled = treeiter is not None
        self.__remove_button.set_sensitive(enabled)
        self.category_name_entry.set_sensitive(enabled)
        self.__category_icon_button.set_sensitive(enabled)
        self.__add_attr_button.set_sensitive(enabled)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle category add
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_category_add(self):

        # show dialog to enter category ID and Name
        dialog = Gtk.Dialog(title='Creating category', modal=True)
        dialog.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
        dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        dialog.set_position(Gtk.WindowPosition.CENTER)
        dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
        dialog.set_border_width(10)

        grid = Gtk.Grid.new()
        grid.set_row_spacing(5)
        grid.set_column_spacing(5)
        grid.show()
        dialog.vbox.pack_start(grid, True, True, 0)

        label = mobius.ui.label('ID')
        label.set_halign(mobius.ui.label.align_right)
        label.set_visible(True)
        grid.attach(label, 0, 0, 1, 1)

        dialog.id_entry = Gtk.Entry()
        dialog.id_entry.show()
        grid.attach(dialog.id_entry, 1, 0, 1, 1)

        label = mobius.ui.label('Name')
        label.set_halign(mobius.ui.label.align_right)
        label.set_visible(True)
        grid.attach(label, 0, 1, 1, 1)

        dialog.name_entry = Gtk.Entry()
        dialog.name_entry.show()
        grid.attach(dialog.name_entry, 1, 1, 1, 1)

        rc = dialog.run()
        cat_id = dialog.id_entry.get_text()
        cat_name = dialog.name_entry.get_text()
        dialog.destroy()

        # if user pressed OK, create category
        if rc == Gtk.ResponseType.OK and cat_id:
            # create category
            category = mobius.framework.new_category(cat_id)
            category.name = cat_name

            # update window
            icon = self.__render_icon(category)
            model = self.category_listview.get_model()
            treeiter = model.append((icon, category.name, category))

            selection = self.category_listview.get_selection()
            selection.select_iter(treeiter)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle category remove
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_category_remove(self):
        selection = self.category_listview.get_selection()
        model, treeiter = selection.get_selected()

        if treeiter:
            category = model.get_value(treeiter, CATEGORY_OBJ)

            # show confirmation dialog
            dialog = mobius.ui.message_dialog(mobius.ui.message_dialog.type_question)
            dialog.text = f"You are about to remove category '{category.name}'. Are you sure?"
            dialog.add_button(mobius.ui.message_dialog.button_yes)
            dialog.add_button(mobius.ui.message_dialog.button_no)
            dialog.set_default_response(mobius.ui.message_dialog.button_no)
            rc = dialog.run()

            if rc != mobius.ui.message_dialog.button_yes:
                return

            # remove category
            mobius.framework.remove_category(category.id)

            # update window
            has_next = model.remove(treeiter)

            if has_next:
                selection.select_iter(treeiter)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle category name edition
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_category_name_changed(self, widget, *args):
        model, treeiter = self.category_listview.get_selection().get_selected()

        if not self.is_selecting and treeiter:
            value = self.category_name_entry.get_text()

            category = model.get_value(treeiter, CATEGORY_OBJ)
            category.name = value
            model.set_value(treeiter, CATEGORY_NAME, value)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle category icon edition
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_category_icon_edit(self):

        # choose file
        fs = Gtk.FileChooserDialog(title='Choose 64x64 icon filename')
        fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        fs.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)

        fc_filter = Gtk.FileFilter()
        fc_filter.add_pattern('*.png')
        fc_filter.add_pattern('*.jpg')
        fc_filter.add_pattern('*.gif')
        fc_filter.add_pattern('*.svg')
        fs.set_filter(fc_filter)

        rc = fs.run()
        filename = fs.get_filename()
        fs.destroy()

        if rc != Gtk.ResponseType.OK:
            return

        # read file
        fp = open(filename, 'rb')
        data = fp.read()
        fp.close()

        # set new icon
        model, treeiter = self.category_listview.get_selection().get_selected()

        if treeiter:
            category = model.get_value(treeiter, CATEGORY_OBJ)
            category.icon_data = resize_icon_data(data)
            icon = mobius.ui.new_icon_from_data(category.icon_data, mobius.ui.icon.size_dnd)
            self.__category_icon_button.set_icon(icon)
            model.set_value(treeiter, CATEGORY_ICON, icon)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief render category icon
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __render_icon(self, category):
        icon = None

        if category.icon_data:
            image = mobius.ui.new_icon_from_data(category.icon_data, mobius.ui.icon.size_dnd)
            icon = image.get_ui_widget().get_pixbuf()

        return icon

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle attribute selection
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_attribute_selection_changed(self, selection, *args):
        model, treeiter = selection.get_selected()

        # get value mask
        if treeiter:
            value_mask = model.get_value(treeiter, ATTR_VALUE_MASK) or ''
            row = model.get_path(treeiter)[0]
        else:
            value_mask = ''
            row = 0

        # set entry text
        self.is_setting_value_mask = True
        self.value_mask_entry.set_text(value_mask)
        self.is_setting_value_mask = False

        # enable/disable widgets
        enabled = treeiter is not None
        up_enabled = enabled and row > 0
        down_enabled = enabled and model.iter_next(treeiter) is not None

        self.value_mask_entry.set_sensitive(enabled)
        self.__remove_attr_button.set_sensitive(enabled)
        self.__up_attr_button.set_sensitive(up_enabled)
        self.__down_attr_button.set_sensitive(down_enabled)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle attribute add
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_attribute_add(self):
        model = self.attribute_listview.get_model()
        treeiter = model.append(('<ID>', '<NAME>', ''))
        path = model.get_path(treeiter)
        column = self.attribute_listview.get_column(ATTR_ID)
        self.attribute_listview.set_cursor(path, column, True)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle attribute remotion
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_attribute_remove(self):
        selection = self.attribute_listview.get_selection()
        model, treeiter = selection.get_selected()

        if treeiter:
            # remove attribute
            attr_id = model.get_value(treeiter, ATTR_ID)
            self.category.remove_attribute(attr_id)

            # update window
            has_next = model.remove(treeiter)

            if has_next:
                selection.select_iter(treeiter)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle move up
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_attribute_up(self):
        model, treeiter = self.attribute_listview.get_selection().get_selected()

        if treeiter:
            # update attribute
            dest_row = model.get_path(treeiter)[0] - 1
            attr_id = model.get_value(treeiter, ATTR_ID)
            attr = self.category.get_attribute(attr_id)
            attr.index = dest_row + 1

            # update window
            dest_iter = model.get_iter(dest_row)
            model.move_before(treeiter, dest_iter)
            self.attribute_listview.scroll_to_cell(dest_row)

            if dest_row == 0:
                self.__up_attr_button.set_sensitive(False)

            self.__down_attr_button.set_sensitive(True)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle move down
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_attribute_down(self):
        model, treeiter = self.attribute_listview.get_selection().get_selected()

        if treeiter:
            # update attribute
            dest_row = model.get_path(treeiter)[0] + 1
            attr_id = model.get_value(treeiter, ATTR_ID)
            attr = self.category.get_attribute(attr_id)
            attr.index = dest_row + 1

            # update window
            dest_iter = model.get_iter(dest_row)
            model.move_after(treeiter, dest_iter)
            self.attribute_listview.scroll_to_cell(dest_row)

            if dest_row + 1 >= len(model):
                self.__down_attr_button.set_sensitive(False)

            self.__up_attr_button.set_sensitive(True)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle attribute ID edition
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_attribute_id_edited(self, cell, path, new_text, *args):
        model = self.attribute_listview.get_model()
        treeiter = model.get_iter_from_string(path)
        text = model.get_value(treeiter, ATTR_ID)

        if text == '<ID>' and new_text.strip() != '' and new_text != '<ID>':
            model.set_value(treeiter, ATTR_ID, new_text)
            attr = self.category.new_attribute(new_text)

        elif text == '<ID>':
            dialog = mobius.ui.message_dialog(mobius.ui.message_dialog.type_error)
            dialog.text = 'Attribute ID is mandatory'
            dialog.add_button(mobius.ui.message_dialog.button_ok)
            rc = dialog.run()

            column = self.attribute_listview.get_column(ATTR_ID)
            GLib.idle_add(self.attribute_listview.set_cursor, path, column, True)

        elif text != new_text:
            dialog = mobius.ui.message_dialog(mobius.ui.message_dialog.type_error)
            dialog.text = 'Cannot changed attribute ID'
            dialog.add_button(mobius.ui.message_dialog.button_ok)
            rc = dialog.run()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle attribute name edition
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_attribute_name_edited(self, cell, path, new_text, *args):
        model = self.attribute_listview.get_model()
        treeiter = model.get_iter_from_string(path)
        text = model.get_value(treeiter, ATTR_NAME)

        if text != new_text:
            attr_id = model.get_value(treeiter, ATTR_ID)
            model.set_value(treeiter, ATTR_NAME, new_text)
            attr = self.category.get_attribute(attr_id)
            attr.name = new_text

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle value_mask edition
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_value_mask_changed(self, widget, *args):
        selection = self.attribute_listview.get_selection()
        model, treeiter = selection.get_selected()

        if not self.is_setting_value_mask and treeiter:
            attr_id = model.get_value(treeiter, ATTR_ID)
            value = self.value_mask_entry.get_text()
            model.set_value(treeiter, ATTR_VALUE_MASK, value)

            attr = self.category.get_attribute(attr_id)
            attr.value_mask = value


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Start function
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def pvt_start():
    icon = pymobius.mediator.call('extension.get-icon-path', EXTENSION_ID)
    mobius.core.add_resource('menu.tools.' + EXTENSION_ID, 'Menu Tool: Category Manager',
                             (icon, EXTENSION_NAME, on_activate))

    # fill database, if necessary
    app = mobius.core.application()

    if not os.path.exists(app.get_config_path("category.sqlite")):
        path = app.get_data_path('data/category.json')
        f = mobius.io.new_file_by_path(path)
        import_json(f)


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Stop function
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def pvt_stop():
    mobius.core.remove_resource('menu.tools.' + EXTENSION_ID)
    pymobius.mediator.call('ui.working-area.del', EXTENSION_ID)


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief on_activate
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def on_activate(item_id):
    working_area = pymobius.mediator.call('ui.working-area.get', EXTENSION_ID)

    if not working_area:
        widget = Widget()
        icon_path = pymobius.mediator.call('extension.get-icon-path', EXTENSION_ID)

        working_area = pymobius.mediator.call('ui.working-area.new', EXTENSION_ID)
        working_area.set_default_size(600, 400)
        working_area.set_title(EXTENSION_NAME)
        working_area.set_icon(icon_path)
        working_area.set_widget(widget)

    working_area.show()
