Pythonのみを使って、今TODOアプリケーション（分散型）を作るとしたら - 拡張その1

[1]:https://github.com/The-town/qiita/tree/master/todo
[2]:https://qiita.com/The-town/items/3bc23ea6eef140787ee2
[3]:https://qiita.com/The-town/items/3bc23ea6eef140787ee2#使い方
[4]:https://stackoverflow.com/questions/5348454/is-it-possible-to-colour-a-specific-item-in-a-listbox-widget

## 本記事の概要

本記事はQiita夏祭り2020の 「 〇〇（言語）のみを使って、今△△（アプリ）を作るとしたら」 を
テーマにして作成した**TODOアプリケーション（分散型）**の**機能拡張**について説明を行うものです。

[Pythonのみを使って、今TODOアプリケーション（分散型）を作るとしたら][2]で執筆したTODOアプリケーションを
機能拡張してみました。
Qiita夏祭りのためだけに作ったつもりが楽しくなってきたので、これからゆっくりと拡張していこうと思います。

使用しているのは**Python3（ver 3.7）**であり、Windows10上で動作確認をしております。

コードは以下のGitHubに配置しております。ご自由にお使いください。

[コード][1]

## 拡張後のTODOアプリケーション（分散型）とは

### 見た目

以下のようなアプリケーションになりました。

![todo_ver2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/0d030463-b826-f032-53cb-6cb8a3c8ac14.jpeg)

### 拡張された機能

拡張した機能は以下の4つになります。

* **TODOファイル**に記載された**メタデータ**の表示
* **期限日**、**重要度**を基にした**TODOファイル**の**ソート**
* **TODOファイル**の名前につけた**重要度**による**色分け**
* **TODO詳細画面**から**パス**をクリックすることで**テキストエディタ**を開く

#### TODOファイルに記載されたメタデータの表示

メタデータは**TODO名**の後に**期限**や**カテゴリ**として追加されています。

![todo_ver2_metadata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/3f394f64-67fe-0fbe-0dad-379e2b4a4d83.png)

**メタデータ**を表示したい場合は**TODO**ファイルの先頭行に**#メタデータ1 メタデータ2**と記載していきます。

```todo.txt
#2020/7/12 修正

todoの詳細
```

**メタデータ**自体は**config.ini**によって管理しています。
**[Meta_data]**部分です。数字（連番）の後に**メタデータ**として表示したい**キー名**を入力します。

この設定ファイルだと**#2020/07/29**と**TODOファイル**の先頭行に記載すれば、**TODO一覧**の該当TODOへ
**期限:2020/07/29**という文字列が追記されます。

1番のメタデータは要らないけど、2番のメタデータは欲しい、つまり**カテゴリ**だけ表示したい場合は、
**#[半角スペース1文字]カテゴリ名**とします。半角スペース文字を区切り文字としているので、
不要なメタデータは**半角スペース1文字**を入れましょう。

```config:config.ini
[Dir_names]
django=F:\Document\800_IT自己学習\09_python\10_Django
kivy=F:\Document\800_IT自己学習\09_python\14_Kivy
qiita=F:\Document\800_IT自己学習\09_python\51_Qiita

[File_names]
todo=*todo*.txt

[Meta_data]
1=期限
2=カテゴリ
```

#### 期限日、重要度を基にしたTODOファイルのソート

左側のプルダウンからソート方法を選択し、更新ボタンを押すことでソートできます。

**importance**は重要度、**limit**は期限です。

![todo_ver2_sort.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/5fdacc19-3cb1-abec-2451-d128c1572abd.png)

**importance**は設定ファイルの**Importance_color**で使用されている**重要度**を使用します。
**重要度**は基本的には**A、B、C**のような**ローマ字**か**数字**にすると分かりやすいかと思います。
ここは設定ファイルを変更することで好きなように出来ます。

```config:config.ini
[Dir_names]
django=F:\Document\800_IT自己学習\09_python\10_Django
kivy=F:\Document\800_IT自己学習\09_python\14_Kivy
qiita=F:\Document\800_IT自己学習\09_python\51_Qiita

[File_names]
todo=*todo*.txt

[Importance_color]
default=white
A=red
B=yellow
C=green

[Meta_data]
1=期限
2=カテゴリ
```

#### TODOファイルの名前につけた重要度による色分け 

これは設定ファイルに記載した**重要度**と**色**の対応をもとに、TODOリストの色を変える機能です。

上記で説明しているように**config.ini**の**[Importance_color]**を設定し、好きな色にすることが出来ます。

#### TODO詳細画面からパスをクリックすることでテキストエディタを開く

TODO詳細画面にファイルパスを記載しており、そこをダブルクリックすることで**OS既定のプログラム**を使用して**TODOファイル**を開くことが出来ます。

私の環境では**.txt**はサクラエディタで開くことになっているため、TODO詳細画面からファイルパスをクリックするとサクラエディタが開きます。

![todo_ver2_open_texteditor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/f3e15d5c-53f4-5d88-c33b-3c4e067519a5.png)

### 使い方

詳しくは[前回の投稿ページ][3]に記載しております。

今回更新した設定ファイルは**Importance_color**と**Meta_data**です。

```config:config.ini
[Dir_names]
#プルダウンに表示される名前=TODOファイルを格納しているフォルダの絶対パス
#例
qiita=F:\Document\800_IT自己学習\09_python\51_Qiita

[File_names]
#TODOリスト一覧に表示したいファイル名。ワイルドカード(*)を使用できます。
#例

#先頭にtodoという文字列があるファイル
todo=todo*

#拡張子がpyのファイル
python=*.py

[Importance_color]
#重要度を表すためにファイル名で使用する文字列と対応する色
#defaultは重要度の文字列が入っていないファイルに対して使用される色です。
#色はredやblueなどの文字列で指定します。
default=white
A=red
B=yellow
C=green

[Meta_data]
#メタデータとして使用するファイル内の文字列の位置とメタデータのキー名
#TODOファイルの先頭行のうち#から始まるものをメタデータとして認識します。
# example: #2020/09/01 機能追加 とすれば期限:2020/09/01 カテゴリ:機能追加となります。
#          # 機能追加 とすればカテゴリ:機能追加となります（半角スペース1文字があるため期限は無視される）。
1=期限
2=カテゴリ

```

## 実装

### TODOファイルに記載されたメタデータの表示

これは単純に**Todo**クラスに**search_mata_data**メソッドを実装し、メタデータのリストを返してもらうようにしました。
メタデータのリストを**listbox.insert**で表示しています。

```python:display.py
...
class TodoDisplay:
    def display_todo(self):
        ...
        for path in paths:
            metadata_list = self.todo.search_meta_data(path)
            insert_statement_list = [path.split("\\")[-1].split(".")[0]]
            insert_statement_list.extend(metadata_list)
            insert_statement = " ".join(insert_statement_list)
            self.listbox.insert(todo_list_box_id, insert_statement)
        ...
...
```

```python:todo.py
class Todo:
    ...

    def search_meta_data(self, path):
        linecache.clearcache()
        first_line = linecache.getline(path, 1)
        if "#" == first_line[0]:
            metadata_list = first_line[1:].split(" ")[:len(self.rule_file["Meta_data"].keys())]
            display_metadata_list = []
            for i, metadata in enumerate(metadata_list):
                if metadata != "":
                    display_metadata_list.append(":".join([self.rule_file["Meta_data"][str(i+1)], metadata]))
            return display_metadata_list
        else:
            return [""]
    ...
```

### 期限日、重要度を基にしたTODOファイルのソート 

**TodoDisplay**クラスから**Todo**クラスにある**sort_todo**メソッドを呼び出します。

引数として**combbox（プルダウン）**で選択されている文字列を渡し、**sort_todo**で渡された文字列に応じて呼び出す関数を変えています。

```python:display.py
class TodoDisplay:
    ...
    def display_todo(self):
        ...
        if self.sort_combbox.get() == "":
            pass
        else:
            paths = self.todo.sort_todo(paths, method=self.sort_combbox.get())
        ...
    ...
```

```python:todo.py
class Todo:
    ...
    def sort_todo(self, paths, method):
        if method == "importance":
            return self.sort_importance(paths)
        elif method == "limit":
            return self.sort_todo_limit(paths)

    def sort_importance(self, paths):
        path_dicts = []
        for path in paths:
            path_dict = {}
            file_name = path.split("\\")[-1].split(".")[0]
            result = self.judge_importance(file_name)
            if result is None:
                path_dict["importance"] = "z"
            else:
                path_dict["importance"] = result.group()[1]
            path_dict["path"] = path
            path_dicts.append(path_dict)

        sorted_path_dicts = sorted(path_dicts, key=lambda x: x["importance"])
        sorted_paths = [sorted_path_dict["path"] for sorted_path_dict in sorted_path_dicts]
        return sorted_paths

    def sort_todo_limit(self, paths):
        path_dicts = []
        for path in paths:
            path_dict = {}
            first_metadata = self.search_meta_data(path)[0]
            if self.rule_file["Meta_data"]["1"] in first_metadata:
                path_dict["metadata_todo_limit"] = first_metadata.split(":")[-1]
            else:
                path_dict["metadata_todo_limit"] = "9999/12/31"
            path_dict["path"] = path
            path_dicts.append(path_dict)

        sorted_path_dicts = sorted(path_dicts, key=lambda x: x["metadata_todo_limit"])
        sorted_paths = [sorted_path_dict["path"] for sorted_path_dict in sorted_path_dicts]
        return sorted_paths
    ...
```


### TODOファイルの名前につけた重要度による色分け

**Todo**クラスの**search_importance**メソッドへファイルパスを渡し、対応する色を返してもらいます。
その後、**listbox.itemconfig**を使用して色付けを行います。

リストボックスの個々の行へ色付けする方法は以下のリンクを参考にしました。

[Is it possible to colour a specific item in a Listbox widget?][4]

```python:display.py
class TodoDisplay:
    ...
    def display_todo(self):
        ...
        for path in paths:
            ...
            importance_color = self.todo.search_importance(path.split("\\")[-1].split(".")[0])
            self.listbox.itemconfig(todo_list_box_id, {'bg': importance_color})
            ...
        ...
    ...
```

### TODO詳細画面からパスをクリックすることでテキストエディタを開く

**text.tag_bind**を使用することで、**イベント**をテキストの文字列に紐づけることができます。
また**os.system("start " + path)**とすることで、**OS**に設定されている既定のプログラムで**path**のファイルを開けます。

```python:gui_object.py
class Listbox(tk.Listbox):
    ...
    def show_detail(self, event=None):
        ...
        self.text = Text(self.master_of_detail_text)
        self.text.tag_config('system_message', background="white", foreground="blue", underline=1)
        self.text.tag_bind("system_message", "<Double-Button-1>", self.open_with_another_app)
        self.text.insert(END, self.get_todo_list()[self.index(ACTIVE)], "system_message")
        ...
    ...
    
    def open_with_another_app(self, event=None):
        path = self.get_todo_list()[self.index(ACTIVE)]
        os.system("start " + path)
```

## 終わりに

なんだか、アプリケーションを成長させるのが楽しくなってきました。
RPGでキャラクターを育てている感覚と同じかもしれません。

これから使っていって、もっと拡張していけたらと思います。そろそろUIもしっかりさせようと。