]> git.phdru.name Git - m_librarian.git/commitdiff
Merge branch 'wx'
authorOleg Broytman <phd@phdru.name>
Sun, 14 Jan 2024 09:44:53 +0000 (12:44 +0300)
committerOleg Broytman <phd@phdru.name>
Sun, 14 Jan 2024 09:44:53 +0000 (12:44 +0300)
[skip ci]

13 files changed:
.gitattributes
docs/install.rst
m_librarian/config.py
m_librarian/wx/AWindow.py [new file with mode: 0644]
m_librarian/wx/Application.py [new file with mode: 0644]
m_librarian/wx/Grids.py [new file with mode: 0644]
m_librarian/wx/ListAuthors.py [new file with mode: 0644]
m_librarian/wx/ListBooks.py [new file with mode: 0644]
m_librarian/wx/SearchPanels.py [new file with mode: 0644]
m_librarian/wx/__init__.py [new file with mode: 0644]
m_librarian/wx/session_config.py [new file with mode: 0755]
scripts/ml-wx.py [new file with mode: 0755]
setup.py

index b7dbfd264a36d61dcdf6eced5b20d09c7100f36c..cf217cc2e689e4abbfea4c2f7a9d975bff0c0ae5 100644 (file)
@@ -1,4 +1,5 @@
 .git* export-ignore
 /README.rus.txt encoding=utf-8
+*.py encoding=utf-8
 *.rst encoding=utf-8
 *.txt text
index 7fbe8d090f3fca4011ac52b881eca7fd272bc555..7e4a26ec5b72148bf75396487469b893b470d847 100644 (file)
@@ -35,6 +35,9 @@ Other extras
 
 ``pip install m_librarian[web]`` installs libraries needed for web-ui.
 
+``pip install m_librarian[wx]`` installs a library needed for wx GUI
+(``wxPython``).
+
 Installation from sources
 =========================
 
index b4a7b603d23d0fe06c4c693fb5081f0b29f9b6ad..a6b88f01091dc0539053089c72d3aed2bb1f0e8b 100755 (executable)
@@ -29,11 +29,11 @@ def find_config_dirs():
     return None
 
 
-def find_config_file(config_dirs=None):
+def find_config_file(config_dirs=None, config_filename='m_librarian.conf'):
     if config_dirs is None:
         config_dirs = find_config_dirs()
     for d in config_dirs:
-        ml_conf_file = os.path.join(d, 'm_librarian.conf')
+        ml_conf_file = os.path.join(d, config_filename)
         if os.path.exists(ml_conf_file):
             return ml_conf_file
     else:
diff --git a/m_librarian/wx/AWindow.py b/m_librarian/wx/AWindow.py
new file mode 100644 (file)
index 0000000..8c46324
--- /dev/null
@@ -0,0 +1,101 @@
+# coding: utf-8
+
+import wx, wx.adv  # noqa: E401 multiple imports on one line
+from ..__version__ import __version__
+from .session_config import get_session_config
+
+
+class AWindow(wx.Frame):
+
+    '''
+    A universal parent class for all top-level application windows
+
+    Standard menu and ability to save/restore window size.
+    '''
+
+    # Subclasses should override these
+    session_config_section_name = None
+    window_title = None
+
+    def __init__(self, parent=None):
+        if self.session_config_section_name:
+            session_config = get_session_config()
+            width = session_config.getint(
+                self.session_config_section_name, 'width', 600)
+            height = session_config.getint(
+                self.session_config_section_name, 'height', 400)
+        else:
+            width = 600
+            height = 400
+        wx.Frame.__init__(
+            self,
+            parent=parent, id=-1, title=self.window_title,
+            size=wx.Size(width=width, height=height),
+        )
+        self.OnInit()
+        self.Show(True)
+
+    def OnInit(self):
+        if self.Parent:
+            self.Parent.Disable()
+        self.InitMenu()
+        if self.session_config_section_name:
+            self.Bind(wx.EVT_SIZE, self.OnSize)
+        self.Bind(wx.EVT_CLOSE, self.OnClose)
+
+    def InitMenu(self):
+        MenuBar = wx.MenuBar()
+        self.SetMenuBar(MenuBar)
+
+        file_menu = wx.Menu()
+
+        if self.Parent:
+            close_win = \
+                file_menu.Append(wx.ID_CLOSE, u"&Закрыть", u"Закрыть окно")
+            self.Bind(wx.EVT_MENU, self.OnCloseCommand, close_win)
+
+        quit = file_menu.Append(wx.ID_EXIT, u"&Выход", u"Выйти из программы")
+        self.Bind(wx.EVT_MENU, self.OnQuit, quit)
+        MenuBar.Append(file_menu, u"&Файл")
+
+        about_menu = wx.Menu()
+        about = about_menu.Append(wx.ID_ABOUT,
+                                  u"&О m_Librarian", u"О m_Librarian")
+        self.Bind(wx.EVT_MENU, self.OnAbout, about)
+        MenuBar.Append(about_menu, u"&О программе")
+
+    def OnCloseCommand(self, event):
+        self.Close(True)
+
+    def OnClose(self, event):
+        if self.Parent:
+            self.Parent.Enable()
+        event.Skip()  # Call other handlers
+
+    def OnQuit(self, event):
+        wx.GetApp().ExitMainLoop()
+
+    def OnAbout(self, event):
+        aboutInfo = wx.adv.AboutDialogInfo()
+        aboutInfo.SetName(u'm_Librarian')
+        aboutInfo.SetVersion(__version__)
+        aboutInfo.SetDescription(
+            u'Библиотекарь для библиотек LibRusEc/Flibusta')
+        aboutInfo.AddDeveloper(u'Олег Бройтман')
+        aboutInfo.SetWebSite(
+            u'https://phdru.name/Software/Python/m_librarian/')
+        aboutInfo.SetCopyright(u'(C) 2023, 2024 Олег Бройтман')
+        aboutInfo.SetLicense(u'GPL')
+        wx.adv.AboutBox(aboutInfo)
+
+    def OnSize(self, event):
+        """Save window size in the session config"""
+        if self.session_config_section_name:
+            size = event.GetSize()
+            session_config = get_session_config()
+            session_config.set(
+                self.session_config_section_name, 'width', str(size.width))
+            session_config.set(
+                self.session_config_section_name, 'height', str(size.height))
+            session_config.save()
+        event.Skip()  # Call other handlers
diff --git a/m_librarian/wx/Application.py b/m_librarian/wx/Application.py
new file mode 100644 (file)
index 0000000..d99edb6
--- /dev/null
@@ -0,0 +1,27 @@
+import wx
+from .AWindow import AWindow
+from .SearchPanels import SearchAuthorsPanel, SearchBooksPanel
+
+
+class MainWindow(AWindow):
+
+    session_config_section_name = 'main_window'
+    window_title = u"m_Librarian"
+
+    def OnInit(self):
+        AWindow.OnInit(self)
+        vsizer = wx.BoxSizer(wx.VERTICAL)
+        self.SetSizer(vsizer)
+
+        search_authors_panel = SearchAuthorsPanel(self)
+        search_books_panel = SearchBooksPanel(self)
+        vsizer.Add(search_authors_panel, 0, wx.EXPAND, 0)
+        vsizer.Add(search_books_panel, 0, wx.EXPAND, 0)
+
+
+class Application(wx.App):
+
+    def OnInit(self):
+        frame = MainWindow()
+        self.SetTopWindow(frame)
+        return True
diff --git a/m_librarian/wx/Grids.py b/m_librarian/wx/Grids.py
new file mode 100644 (file)
index 0000000..fe2f8d0
--- /dev/null
@@ -0,0 +1,58 @@
+import wx, wx.grid  # noqa: E401 multiple imports on one line
+from .AWindow import AWindow
+
+
+class GridPanel(wx.Panel):
+
+    def __init__(self, parent, param):
+        wx.Panel.__init__(self, parent)
+        self.param = param
+
+        vsizer = wx.BoxSizer(wx.VERTICAL)
+        self.SetSizer(vsizer)
+
+        self.grid = grid = wx.grid.Grid(self)
+        vsizer.Add(grid, 0, wx.EXPAND, 0)
+
+        grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.OnDClick)
+        grid.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
+
+        self.InitGrid()
+        grid.SetFocus()
+
+        parent.Bind(wx.EVT_ACTIVATE, self.OnActivate)
+        parent.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus)
+        self.Bind(wx.EVT_ACTIVATE, self.OnActivate)
+        self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus)
+
+    def InitGrid(self):
+        raise NotImplementedError
+
+    def OnDClick(self, event):
+        raise NotImplementedError
+
+    def OnKeyDown(self, event):
+        raise NotImplementedError
+
+    def OnActivate(self, event):
+        if event.GetActive():
+            self.grid.SetFocus()
+
+    def OnSetFocus(self, event):
+        self.grid.SetFocus()
+
+
+class GridWindow(AWindow):
+
+    # Subclasses must override these
+    session_config_section_name = None
+    window_title = None
+    GridPanelClass = GridPanel
+
+    def __init__(self, parent, param):
+        self.param = param
+        AWindow.__init__(self, parent)
+
+    def OnInit(self):
+        AWindow.OnInit(self)
+        self.panel = self.GridPanelClass(self, self.param)
diff --git a/m_librarian/wx/ListAuthors.py b/m_librarian/wx/ListAuthors.py
new file mode 100644 (file)
index 0000000..8f18a7a
--- /dev/null
@@ -0,0 +1,63 @@
+# coding: utf-8
+
+import wx, wx.grid  # noqa: E401 multiple imports on one line
+from ..compat import string_type, unicode_type
+from ..search import books_by_author
+from ..translations import translations
+from .Grids import GridWindow, GridPanel
+from .ListBooks import ListBooksWindow
+
+
+class ListAuthorsPanel(GridPanel):
+
+    def InitGrid(self):
+        _ = getattr(translations, 'ugettext', None) or translations.gettext
+        authors = self.param['authors']
+        columns = self.param['columns']
+        grid = self.grid
+        grid.CreateGrid(len(authors), len(columns))
+        grid.EnableEditing(False)
+        for row in range(len(authors)):
+            grid.SetRowLabelValue(row, str(row))
+            grid.AutoSizeRowLabelSize(row)
+        for col, col_name in enumerate(columns):
+            grid.SetColLabelValue(col, _(col_name))
+            grid.AutoSizeColLabelSize(col)
+            if col_name == 'count':
+                cell_attr = wx.grid.GridCellAttr()
+                cell_attr.SetAlignment(wx.ALIGN_RIGHT, wx. ALIGN_CENTRE)
+                grid.SetColAttr(col, cell_attr)
+        for row, author in enumerate(authors):
+            for col, col_name in enumerate(columns):
+                value = getattr(author, col_name)
+                if not isinstance(value, (string_type, unicode_type)):
+                    value = str(value)
+                grid.SetCellValue(row, col, value)
+        grid.AutoSizeColumns()
+        grid.AutoSizeRows()
+
+    def listBooks(self, row):
+        authors = self.param['authors']
+        author = authors[row]
+        _books_by_author = books_by_author(author.id)
+        ListBooksWindow(self, _books_by_author)
+
+    def OnDClick(self, event):
+        row = event.GetRow()
+        self.listBooks(row)
+
+    def OnKeyDown(self, event):
+        if event.GetKeyCode() == wx.WXK_ESCAPE:
+            self.Parent.Close()
+        elif event.GetKeyCode() in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER):
+            row = self.grid.GetGridCursorRow()
+            self.listBooks(row)
+        else:
+            event.Skip()
+
+
+class ListAuthorsWindow(GridWindow):
+
+    session_config_section_name = 'list_authors'
+    window_title = u"m_Librarian: Список авторов"
+    GridPanelClass = ListAuthorsPanel
diff --git a/m_librarian/wx/ListBooks.py b/m_librarian/wx/ListBooks.py
new file mode 100644 (file)
index 0000000..4252c2a
--- /dev/null
@@ -0,0 +1,209 @@
+# coding: utf-8
+
+import wx, wx.grid  # noqa: E401 multiple imports on one line
+from ..compat import string_type, unicode_type
+from ..download import download
+from ..translations import translations
+from .Grids import GridWindow, GridPanel
+
+
+_ = getattr(translations, 'ugettext', None) or translations.gettext
+
+
+class BooksDataTable(wx.grid.GridTableBase):
+    def __init__(self, rows_count, column_names):
+        wx.grid.GridTableBase.__init__(self)
+        self.rows_count = rows_count
+        self.column_names = column_names
+        self.data = []
+        for row in range(rows_count + 1):
+            row_data = []
+            self.data.append(row_data)
+            for col in range(len(column_names)):
+                row_data.append('')
+
+    # required methods for the wxPyGridTableBase interface
+
+    def GetNumberRows(self):
+        return self.rows_count
+
+    def GetNumberCols(self):
+        return len(self.column_names)
+
+    def IsEmptyCell(self, row, col):
+        return False
+
+    # Get/Set values in the table.  The Python version of these
+    # methods can handle any data-type, (as long as the Editor and
+    # Renderer understands the type too,) not just strings as in the
+    # C++ version.
+    def GetValue(self, row, col):
+        return self.data[row][col]
+
+    def SetValue(self, row, col, value):
+        self.data[row][col] = value
+
+    # Optional methods
+
+    # Called when the grid needs to display labels
+    def GetRowLabelValue(self, row):
+        return str(row)
+
+    def GetColLabelValue(self, col):
+        return _(self.column_names[col])
+
+    # Called to determine the kind of editor/renderer to use by
+    # default, doesn't necessarily have to be the same type used
+    # natively by the editor/renderer if they know how to convert.
+    def GetTypeName(self, row, col):
+        if col == 0:
+            return wx.grid.GRID_VALUE_BOOL
+        else:
+            return wx.grid.GRID_VALUE_STRING
+
+    # Called to determine how the data can be fetched and stored by the
+    # editor and renderer.  This allows you to enforce some type-safety
+    # in the grid.
+    def CanGetValueAs(self, row, col, typeName):
+        colType = self.GetTypeName(row, col)
+        if typeName == colType:
+            return True
+        else:
+            return False
+
+    def CanSetValueAs(self, row, col, typeName):
+        return self.CanGetValueAs(row, col, typeName)
+
+
+class ListBooksPanel(GridPanel):
+
+    def InitGrid(self):
+        books_by_author = self.param['books_by_author']
+        columns = self.param['columns']
+        columns.insert(0, u'Выбрать')
+        total_rows = 0
+        for author in books_by_author:
+            books = books_by_author[author]
+            series = {book.series for book in books}
+            total_rows += len(books) + len(series) + 1
+        grid = self.grid
+        grid.SetTable(
+            BooksDataTable(total_rows+1, columns),
+            takeOwnership=True,
+        )
+        grid.EnableEditing(False)
+        for col, col_name in enumerate(columns):
+            grid.AutoSizeColLabelSize(col)
+            if col == 0:
+                cell_attr = wx.grid.GridCellAttr()
+                cell_attr.SetAlignment(wx.ALIGN_CENTRE, wx. ALIGN_CENTRE)
+                grid.SetColAttr(col, cell_attr)
+            elif col_name in ('ser_no', 'size'):
+                cell_attr = wx.grid.GridCellAttr()
+                cell_attr.SetAlignment(wx.ALIGN_RIGHT, wx. ALIGN_CENTRE)
+                grid.SetColAttr(col, cell_attr)
+        row = 0
+        grid.SetCellAlignment(row, 1, wx.ALIGN_CENTRE, wx. ALIGN_CENTRE)
+        grid.SetCellSize(row, 1, 1, len(columns)-1)
+        row = 1
+        self.book_by_row = book_by_row = {}  # map {row: book}
+        self.toggle_rows = toggle_rows = {}  # map {row: [list of subrows]}
+        for author in sorted(books_by_author):
+            grid.SetCellAlignment(row, 1, wx.ALIGN_LEFT, wx. ALIGN_CENTRE)
+            grid.SetCellSize(row, 1, 1, len(columns)-1)
+            grid.SetCellValue(row, 1, u'%s' % (author,))
+            author_row = row
+            toggle_rows[author_row] = []
+            row += 1
+            books = books_by_author[author]
+            series = None
+            for book in books:
+                if book.series != series:
+                    if book.series:
+                        value = book.series
+                    else:
+                        value = u'Вне серий'
+                    grid.SetCellAlignment(row, 1,
+                                          wx.ALIGN_LEFT, wx. ALIGN_CENTRE)
+                    grid.SetCellSize(row, 1, 1, len(columns)-1)
+                    grid.SetCellValue(row, 1,
+                                      u'%s — %s' % (book.author1, value))
+                    series_row = row
+                    toggle_rows[author_row].append(row)
+                    toggle_rows[series_row] = []
+                    row += 1
+                    series = book.series
+                for col, col_name in enumerate(columns[1:]):
+                    value = getattr(book, col_name)
+                    if value is None:
+                        value = u''
+                    elif not isinstance(value, (string_type, unicode_type)):
+                        value = str(value)
+                    grid.SetCellValue(row, col+1, value)
+                book_by_row[row] = book
+                toggle_rows[author_row].append(row)
+                toggle_rows[series_row].append(row)
+                row += 1
+        toggle_rows[0] = [row_ for row_ in range(1, row)]
+        grid.AutoSizeColumns()
+        grid.AutoSizeRows()
+        grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnClick)
+
+        search_button = wx.Button(self, label=u'Скачать')
+        self.GetSizer().Add(search_button, 0, wx.ALIGN_CENTER, 0)
+        search_button.Bind(wx.EVT_BUTTON, self.Download)
+
+    def toggleCB(self, row):
+        value = self.grid.GetCellValue(row, 0)
+        if value:
+            value = ''
+        else:
+            value = '1'
+        self.grid.SetCellValue(row, 0, value)
+        toggle_rows = self.toggle_rows
+        if row in toggle_rows:
+            for row_ in toggle_rows[row]:
+                self.grid.SetCellValue(row_, 0, value)
+
+    def OnClick(self, event):
+        if event.GetCol() > 0:
+            return
+        self.toggleCB(event.GetRow())
+
+    def OnDClick(self, event):
+        if event.GetCol() == 0:
+            return
+        self.toggleCB(event.GetRow())
+
+    def OnKeyDown(self, event):
+        if event.GetKeyCode() == wx.WXK_ESCAPE:
+            self.Parent.Close()
+        else:
+            event.Skip()
+
+    def Download(self, event):
+        book_by_row = self.book_by_row
+        found_books = False
+        try:
+            for row in self.toggle_rows[0]:
+                value = self.grid.GetCellValue(row, 0)
+                if value and row in book_by_row:
+                    found_books = True
+                    download(book_by_row[row])
+        except Exception as e:
+            self.report_error(str(e))
+        else:
+            if not found_books:
+                self.report_error(u'Не выбрано книг для сохранения.')
+
+    def report_error(self, error):
+        wx.MessageBox(
+            error, caption='m_Librarian download error',
+            style=wx.OK|wx.ICON_ERROR, parent=self.Parent)
+
+
+class ListBooksWindow(GridWindow):
+
+    session_config_section_name = 'list_books'
+    window_title = u"m_Librarian: Список книг"
+    GridPanelClass = ListBooksPanel
diff --git a/m_librarian/wx/SearchPanels.py b/m_librarian/wx/SearchPanels.py
new file mode 100644 (file)
index 0000000..06f9021
--- /dev/null
@@ -0,0 +1,91 @@
+# coding: utf-8
+
+import wx
+from ..config import get_config
+from ..search import search_authors_raw, search_books_raw
+from .ListAuthors import ListAuthorsWindow, ListBooksWindow
+
+
+_search_types = ['start', 'substring', 'full']
+
+
+class SearchPanel(wx.Panel):
+
+    # Subclasses must override these
+    search_title = None
+    search_button_title = None
+
+    def __init__(self, parent):
+        wx.Panel.__init__(self, parent)
+        search_vsizer = \
+            wx.StaticBoxSizer(wx.VERTICAL, self, self.search_title)
+        self.SetSizer(search_vsizer)
+
+        self.search = search = \
+            wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER)
+        search_vsizer.Add(search, 0, wx.EXPAND, 0)
+        search.Bind(wx.EVT_TEXT_ENTER, self.DoSearch)
+
+        self.search_substr = search_substr = wx.RadioBox(
+            self,
+            choices=[
+                u'Подстрока в начале',
+                u'Подстрока',
+                u'Точное совпадение',
+            ],
+            majorDimension=1, style=wx.RA_SPECIFY_ROWS
+        )
+        search_vsizer.Add(search_substr)
+
+        self.search_case = search_case = wx.CheckBox(
+            self, label=u'Различать прописные/строчные')
+        search_vsizer.Add(search_case)
+
+        search_button = wx.Button(self, label=self.search_button_title)
+        search_vsizer.Add(search_button, 0, wx.ALIGN_CENTER, 0)
+        search_button.Bind(wx.EVT_BUTTON, self.DoSearch)
+
+    def DoSearch(self, event):
+        search = self.search.GetValue()
+        if not search:
+            self.search.SetFocus()
+            return
+        search_substr = _search_types[self.search_substr.GetSelection()]
+        search_case = self.search_case.GetValue()
+        if search_case is False:
+            search_case = None
+        self.realSearch(search, search_substr, search_case)
+
+
+class SearchAuthorsPanel(SearchPanel):
+
+    search_title = u'Поиск авторов'
+    search_button_title = u'Искать авторов'
+
+    def realSearch(self, value, search_substr, search_case):
+        search_results = \
+            search_authors_raw(value, search_substr, search_case)
+        ListAuthorsWindow(self.Parent, search_results)
+
+
+class SearchBooksPanel(SearchPanel):
+
+    search_title = u'Поиск книг'
+    search_button_title = u'Искать книги'
+
+    def __init__(self, parent):
+        SearchPanel.__init__(self, parent)
+        self.use_filters = use_filters = wx.CheckBox(
+            self, label=u'Использовать фильтры')
+        use_filters_cfg = \
+            get_config().getint('filters', 'use_in_search_forms', 1)
+        use_filters.SetValue(use_filters_cfg)
+        sizer = self.GetSizer()
+        s_count = len(sizer.GetChildren())
+        sizer.Insert(s_count-1, use_filters)  # Insert before the cutton
+
+    def realSearch(self, value, search_substr, search_case):
+        use_filters = self.use_filters.GetValue()
+        search_results = \
+            search_books_raw(value, search_substr, search_case, use_filters)
+        ListBooksWindow(self.Parent, search_results)
diff --git a/m_librarian/wx/__init__.py b/m_librarian/wx/__init__.py
new file mode 100644 (file)
index 0000000..792d600
--- /dev/null
@@ -0,0 +1 @@
+#
diff --git a/m_librarian/wx/session_config.py b/m_librarian/wx/session_config.py
new file mode 100755 (executable)
index 0000000..93b725c
--- /dev/null
@@ -0,0 +1,73 @@
+#! /usr/bin/env python
+
+from __future__ import print_function
+import os
+
+from ..config import RawConfigParser, ConfigWrapper, find_config_file
+
+
+__all__ = ['get_session_config']
+
+
+def _find_config_dirs_posix():
+    config_dirs = []
+    if 'XDG_CACHE_HOME' in os.environ:
+        config_dirs.append(os.environ['XDG_CACHE_HOME'])
+    home_cache = os.path.expanduser('~/.cache')
+    if home_cache not in config_dirs:
+        config_dirs.append(home_cache)
+    return config_dirs
+
+
+def _find_config_dirs():
+    if os.name == 'posix':
+        return _find_config_dirs_posix()
+    return None
+
+
+_ml_session_config = None
+
+
+class SessionConfigWrapper(ConfigWrapper):
+    def __init__(self, config, config_path):
+        ConfigWrapper.__init__(self, config)
+        self.config_path = config_path
+
+    def set(self, section, option, value):
+        if not self.config.has_section(section):
+            self.config.add_section(section)
+        super(SessionConfigWrapper, self).set(section, option, value)
+
+    def save(self):
+        if self.config_path is None:
+            config_dirs = _find_config_dirs()
+            self.config_path = \
+                os.path.join(config_dirs[0], 'm_librarian_session.conf')
+        with open(self.config_path, 'wt') as fp:
+            self.config.write(fp)
+
+
+def get_session_config(config_path=None):
+    global _ml_session_config
+    if _ml_session_config is None:
+        _ml_session_config = RawConfigParser()
+        if config_path is None:
+            config_dirs = _find_config_dirs()
+            config_path = \
+                find_config_file(config_dirs, 'm_librarian_session.conf')
+        if config_path is not None:
+            _ml_session_config.read(config_path)
+        _ml_session_config = \
+            SessionConfigWrapper(_ml_session_config, config_path)
+    return _ml_session_config
+
+
+def _test():
+    config_dirs = _find_config_dirs()
+    print("Config dirs:", config_dirs)
+    print("Config file:",
+          find_config_file(config_dirs, 'm_librarian_session.conf'))
+
+
+if __name__ == '__main__':
+    _test()
diff --git a/scripts/ml-wx.py b/scripts/ml-wx.py
new file mode 100755 (executable)
index 0000000..c0a6b46
--- /dev/null
@@ -0,0 +1,18 @@
+#! /usr/bin/env python
+
+import sys
+
+from m_librarian.db import open_db
+from m_librarian.wx.Application import Application
+
+
+def main():
+    if len(sys.argv) > 1:
+        sys.exit("This program doesn't accept any arguments")
+    open_db()
+    app = Application()
+    app.MainLoop()
+
+
+if __name__ == '__main__':
+    main()
index 2dc274f0984b0d4d80153ad8ef7a5c492508116a..867039761f1d0ae2eaf3b655c4735572e461bf3c 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -79,5 +79,6 @@ setup(
     extras_require={
         'pbar': ['m_lib>=3.1'],
         'web': ['bottle', 'CT3'],
+        'wx': ['wxPython'],
     },
 )