Window = 창

Window = 창

2025년 4월 29일

윈도우 #

이전에 구현했던 Layer와 함께 구현했던 Window는 픽셀들이 올려진 도화지라고 볼 수 있고 여기에서 말하는 윈도우는 보통 우리가 생각하는 그 윈도우가 맞다.

구현 방법 #

사실 구현 자체는 별게 없다. Window 클래스의 크기에 맞춰서 미리 그려진 사각형 픽셀을 그릴 뿐이다.

 1  const char close_button[kCloseButtonHeight][kCloseButtonWidth + 1] = {
 2    "...............@",
 3    ".:::::::::::::$@",
 4    ".:::::::::::::$@",
 5    ".:::@@::::@@::$@",
 6    ".::::@@::@@:::$@",
 7    ".:::::@@@@::::$@",
 8    ".::::::@@:::::$@",
 9    ".:::::@@@@::::$@",
10    ".::::@@::@@:::$@",
11    ".:::@@::::@@::$@",
12    ".:::::::::::::$@",
13    ".:::::::::::::$@",
14    ".$$$$$$$$$$$$$$@",
15    "@@@@@@@@@@@@@@@@",
16  };
17
18void DrawWindow(PixelWriter& writer, const char* title) {
19  auto fill_rect = [&writer](Vector2D<int> pos, Vector2D<int> size, uint32_t c) {
20    FillRectangle(writer, pos, size, ToColor(c));
21  };
22  const auto win_w = writer.Width();
23  const auto win_h = writer.Height();
24
25  fill_rect({0, 0},         {win_w, 1},             0xc6c6c6);    // ----------------------
26  fill_rect({1, 1},         {win_w - 2, 1},         0xffffff);    //  --------------------
27  fill_rect({2, 2},         {win_w - 4, win_h - 4}, 0xc6c6c6);    //   ------------------
28  fill_rect({0, 0},         {1, win_h},             0xc6c6c6);    // |
29  fill_rect({1, 1},         {1, win_h - 2},         0xffffff);    //  |
30  fill_rect({win_w - 1, 0}, {1, win_h},             0x000000);    //                      |
31  fill_rect({win_w - 2, 1}, {1, win_h - 2},         0x848484);    //                     |
32  fill_rect({3, 3},         {win_w - 6, 18},        0x000084);    //   ==================
33  fill_rect({1, win_h - 2}, {win_w - 2, 1},         0x848484);    //  --------------------
34  fill_rect({0, win_h - 1}, {win_w, 1},             0x000000);    // ----------------------
35
36  WriteString(writer, {24, 4}, title, ToColor(0xffffff));
37  
38  for (int y = 0; y < kCloseButtonHeight; ++y) {
39    for (int x = 0; x < kCloseButtonWidth; ++x) {
40      PixelColor c = ToColor(0xffffff);
41      if (close_button[y][x] == '@') {
42        c = ToColor(0x000000);
43      } else if (close_button[y][x] == '$') {
44        c = ToColor(0x848484);
45      } else if (close_button[y][x] == ':') {
46        c = ToColor(0xc6c6c6);
47      }
48      writer.Write({win_w - 5 - kCloseButtonWidth + x, 5 + y}, c);
49    }
50  }
51}

사용할 땐 main_window 라는 윈도우 클래스를 하나 만들고, 위에서 구현한 틀을 그려주는 함수를 호출하여 main_window에 그려준다.

LayerManager에 추가해주기만 하면 window에 그려져있는 그림대로 screen에 자동으로 렌더링이 될 것이다.

 1  auto main_window = std::make_shared<Window>(
 2    260, 100, frame_buffer_config.pixel_format);
 3  DrawWindow(*main_window->Writer(), "Hello Window");
 4  WriteString(*main_window->Writer(), {24, 28}, "Welcome to", {0, 0, 0});
 5  WriteString(*main_window->Writer(), {24, 44}, " MinOS World!!", {0, 0, 0});
 6
 7  auto main_window_layer_id = layer_manager->NewLayer()
 8    .SetWindow(main_window)
 9    .SetDraggable(true)
10    .Move({300, 100})
11    .ID();

완성된 윈도우. 605c6a41-eb18-45be-af9d-067b6d91216e


드래그 기능 #

윈도우는 역시 드래그 기능이 있어야 한다.
드라이버를 통한 마우스의 입력은 이미 받을 수 있기 때문에 윈도우를 클릭했을 때 마우스를 움직이면 선택된 윈도우도 옮겨져야 한다.

움직이는건 Layer::Move 를 호출하기만 하면 되는데, 현재 마우스가 클릭한 시점에 마우스 포인터가 가리키는 윈도우를 찾아야한다.

 1// pos 에 있는 윈도우 Layer를 찾아 리턴하는 함수
 2Layer* LayerManager::FindLayerByPosition(Vector2D<int> pos, unsigned int exclude_id) const {
 3  auto pred = [pos, exclude_id](Layer* layer) {
 4    // 마우스 레이어도 윈도우이기 때문에 exclude_id 에는 mouse_layer_id 가 전달된다. 
 5    // 마우스 레이어는 제외
 6    if (layer->ID() == exclude_id) {
 7      return false;
 8    }
 9    const auto& win = layer->GetWindow();
10    if (!win) {
11      return false;
12    }
13    // 현재 레이어의 위치가 마우스 포인터의 범위에 있는지 체크
14    const auto win_pos = layer->GetPosition();
15    const auto win_end_pos = win_pos + win->Size();
16    return win_pos.x <= pos.x && pos.x < win_end_pos.x &&
17            win_pos.y <= pos.y && pos.y < win_end_pos.y;
18  };
19  // layer_stack_의 뒤에서부터 검색해서 zindex가 상위에 있는 레이어 먼저 선택되도록 함
20  auto it = std::find_if(layer_stack_.rbegin(), layer_stack_.rend(), pred);
21  if (it == layer_stack_.rend()) {
22    return nullptr;
23  }
24  return *it;
25}

이제 마우스 옵저버에서 이벤트에 따라 생각하던 동작을 구현하면 된다.

마우스 클릭이 감지되면 현재 포인터 위치의 layer를 선택해서 마우스의 이동량만큼 이동시킨다.
마우스 클릭이 윈도우의 어떤 위치를 클릭할지 모르기 때문에 윈도우는 상대좌표로 이동시켜야 한다.

만약 마우스 위치로 이동시켜버리면 포인터의 시작지점이 윈도우의 시작지점이 되도록 이동된다.

 1// buttons 는 마우스 버튼의 상태를 나타낸다. 
 2void MouseObserver(uint8_t buttons, int8_t displacement_x, int8_t displacement_y) {
 3  static unsigned int mouse_drag_layer_id = 0;
 4  static uint8_t previous_buttons = 0;
 5
 6  const auto oldpos = mouse_position;
 7  auto newpos = mouse_position + Vector2D<int>{displacement_x, displacement_y};
 8  newpos = ElementMin(newpos, screen_size + Vector2D<int>{-1, -1});
 9  mouse_position = ElementMax(newpos,  {0, 0});
10
11  const auto posdiff = mouse_position - oldpos;
12
13  layer_manager->Move(mouse_layer_id, mouse_position);
14
15  // 0x01이 buttons 이벤트의 좌클릭 Pressed를 의미한다.
16  // 이전 버튼 이벤트와 지금 버튼 이벤트에서 좌클릭이 감지됐는지 체크
17  const bool previous_left_pressed = (previous_buttons & 0x01);
18  const bool left_pressed = (buttons & 0x01);
19  if (!previous_left_pressed && left_pressed) {
20    // 처음 좌클릭을 하는 경우 마우스 포인터의 위치에 있는 레이어를 선택한다. 
21    auto layer = layer_manager->FindLayerByPosition(mouse_position, mouse_layer_id);
22    if (layer && layer->IsDraggable()) {
23      mouse_drag_layer_id = layer->ID();
24    }
25  } else if (previous_left_pressed && left_pressed) {
26    // 마우스의 클릭이 유지되는 동안 선택된 layer가 있다면 레이어를 이동시킴
27    // 마우스가 해당 윈도우의 어떤 위치에서 클릭을 시작할지 모르기 때문에 상대주소로 이동시켜야 한다. 
28    // 마우스의 이동량만큼만 이동시킨다.
29    if (mouse_drag_layer_id > 0) {
30      layer_manager->MoveRelative(mouse_drag_layer_id, posdiff);
31    }
32  } else if (previous_left_pressed && !left_pressed) {
33    mouse_drag_layer_id = 0;
34  }
35
36  previous_buttons = buttons;
37}

윈도우의 활성 여부 관리 #

윈도우는 마우스로 선택하면 그때 활성화 되어야한다. 활성화가 되어있다면 타이틀바를 이용해서 표시해줘야 하고, 키보드 입력은 활성화된 윈도우가 받도록 구현해야한다.


활성화 상태 관리 #

활성화된 레이어의 정보를 담는 ActiveLayer 클래스 작성

 1class ActiveLayer {
 2public:
 3  ActiveLayer(LayerManager& manager);
 4  void SetMouseLayer(unsigned int mouse_layer);
 5  void Activate(unsigned int layer_id);
 6  unsigned int GetActive() const { return active_layer_; }
 7
 8private:
 9  LayerManager& manager_;
10  unsigned int active_layer_{0};
11  unsigned int mouse_layer_{0};
12};
13
14extern ActiveLayer* active_layer;
15
16void ActiveLayer::Activate(unsigned int layer_id) {
17  if (active_layer_ == layer_id) {
18    return;
19  }
20  // layer_id는 1 부터 시작한다.
21  if (active_layer_ > 0) {
22    // 이전 레이어를 비활성화 하는 로직
23    Layer* layer = manager_.FindLayer(active_layer_);
24    // 윈도우도 비활성화 세팅을 해줘야한다.
25    layer->GetWindow()->Deactivate();
26    // 해당되는 영역을 다시 그린다
27    manager_.Draw(active_layer_);
28  }
29
30  // 활성화 레이어를 갈아낀다. 
31  active_layer_ = layer_id;
32  if (active_layer_ > 0) {
33    // 현재 레이어를 활성화하는 로직
34    Layer* layer = manager_.FindLayer(active_layer_);
35    // 윈도우도 활성화 세팅을 해줘야한다.
36    layer->GetWindow()->Activate();
37    // 레이어 스택에서 맨 위(마우스보다는 아래)에 위치하도록 zindex 변경
38    // mouse_layer_ 는 Activate 함수가 호출되기 전에 미리 세팅되어 있어야 한다.
39    // 기본값이 0이기 때문에 세팅 전에 사용하면 Activate한 윈도우가 숨겨진다.
40    manager_.UpDown(active_layer_, manager_.GetHeight(mouse_layer_) - 1);
41    // 해당되는 영역을 다시 그린다
42    manager_.Draw(active_layer_);
43  }
44}

마우스 클릭으로 윈도우 활성화 #

마우스 인터럽트가 발생했을 때 클릭한 윈도우 레이어에 대해 활성화 함수 호출

 1void Mouse::OnInterrupt(...) {
 2  // ...
 3  if (!previous_left_pressed && left_pressed) {
 4    auto layer = layer_manager->FindLayerByPosition(position_, layer_id_);
 5    if (layer && layer->IsDraggable()) {
 6      drag_layer_id_ = layer->ID();
 7      // Draggable 하다면 해당되는 윈도우 활성화
 8      active_layer->Activate(layer->ID());
 9    } else {
10      // 아닌경우 모든 윈도우 비활성화
11      active_layer->Activate(0);
12    }
13  // ...
14}
15
16// Window 활성화 함수
17void ToplevelWindow::Activate() {
18  Window::Activate();
19  DrawWindowTitle(*Writer(), title_.c_str(), true);
20}
21// Window 비활성화 함수
22void ToplevelWindow::Deactivate() {
23  Window::Deactivate();
24  DrawWindowTitle(*Writer(), title_.c_str(), false);
25}
26
27// 타이틀바를 그릴 때 active 상태에 따라 bgcolor를 다르게 설정한다.
28void DrawWindowTitle(PixelWriter& writer, const char* title, bool active) {
29  const auto win_w = writer.Width();
30  uint32_t bgcolor = 0x848484;   // 비활성화 색
31  if (active) {
32    bgcolor = 0x000084;          // 활성화 색
33  }
34  // ... 이전 Draw와 동일
35}
36
37void DrawWindow(PixelWriter& writer, const char* title) {
38  // ... 이전 Draw와 동일
39  // Activate가 아닌 DrawWindow를 통해서 들어오는 경우(가렸다가 다시 보일때) 비활성으로 판단한다.
40  // 활성 레이어는 무조건 Activate를 통해 타이틀을 그리고 Inner만 업데이트한다.
41  DrawWindowTitle(writer, title, false);
42}

활성화된 윈도우가 (마우스 제외)가장 위로 올라가고, 쨍한 파란색으로 타이틀바를 그린다.
c3880f25-6d56-4066-a027-1cb5cdf0729d


텍스트 윈도우 #

텍스트 윈도우는 기존 윈도우에 텍스트 박스만 그린 것인데, 텍스트 박스는 그냥 DrawRectangle 같은걸로 그리면 된다.

그리고 text_window의 Writer 와 현재 text_window_index를 이용해서 텍스트 박스의 위치에 정확히 그리는것도 가능하다.

텍스트 커서도 역시 같은 원리로 마지막 글자 위치에 맞게 그릴 수 있다.

 1int text_window_index;
 2void DrawTextCursor(bool visible) {
 3  const auto color = visible ? ToColor(0) : ToColor(0xffffff);
 4  const auto pos = Vector2D<int>{8 + 8*text_window_index, 24 + 5};
 5  FillRectangle(*text_window->Writer(), pos, {7, 15}, color);
 6}
 7
 8void InputTextWindow(char c) {
 9  if (c == 0) {
10    return;
11  }
12  auto pos = []() { return Vector2D<int>{8 + 8*text_window_index, 24 + 6}; };
13  // 최대 문자열 길이는 text_window의 사이즈에 따라 달려있다. 
14  const int max_chars = (text_window->Width() - 16) / 8 - 1;
15  if (c == '\b' && text_window_index > 0) {
16    // 백스페이스 입력된 경우
17    DrawTextCursor(false);
18    // 인덱스 줄이고, 그냥 흰색 사각형으로 지워버림
19    --text_window_index;
20    FillRectangle(*text_window->Writer(), pos(), {8, 16}, ToColor(0xffffff));
21    DrawTextCursor(true);
22  } else if (c >= ' ' && text_window_index < max_chars) {
23    DrawTextCursor(false);
24    // ascii 문자를 입력받은 경우 글자를 쓰고, 인덱스를 증가시킴
25    WriteAscii(*text_window->Writer(), pos(), c, ToColor(0));
26    ++text_window_index;
27    DrawTextCursor(true);
28  }
29  // 글자를 텍스트 윈도우에 썼으니 스크린에도 반영해야 한다.
30  layer_manager->Draw(text_window_layer_id);
31}

깜빡이는 커서 #

이것도 사실 타이머를 이용하면 어렵지 않다.
목표는 1초에 한번 깜빡이도록 구현하는 것이다.

 1  const int kTextboxCursorTimer = 17;
 2  const int kTimer05Sec = static_cast<int>(kTimerFreq * 0.5);
 3  __asm__("cli");
 4  // 0.5 초 짜리 타이머 등록. 
 5  // 내부에서 타이머 큐를 사용하는데 인터럽트에서도 접근하기 때문에 크리티컬 섹션 처리
 6  timer_manager->AddTimer(Timer{kTimer05Sec, kTextboxCursorTimer});
 7  __asm__("sti");
 8  bool textbox_cursor_visible = false;
 9
10  char str[128];
11  // event loop
12  while (true) {
13    switch (msg.type) {
14    case Message::kTimerTimeout:
15      // 텍스트 박스의 타이머인 경우에만
16      if (msg.arg.timer.value == kTextboxCursorTimer) {
17        __asm__("cli");
18        // 끝나면 다시 0.5초 뒤에 등록 
19        timer_manager->AddTimer(
20          Timer{msg.arg.timer.timeout + kTimer05Sec, kTextboxCursorTimer});
21        __asm__("sti");
22        // 전역으로 관리하는 textbox_cursor_visible 값을 반전시킨다. 
23        // 이 로직을 들어올때마다(0.5s) 한번은 visible, 한번은 invisible로 그려진다.
24        textbox_cursor_visible = !textbox_cursor_visible;
25        DrawTextCursor(textbox_cursor_visible);
26        // 텍스트 박스를 다시 그린다.
27        layer_manager->Draw(text_window_layer_id);
28      }
29      break;
30    }
31  }

e4233579-1d3c-432a-846f-a25ddc2be7c4


터미널 윈도우 #

이전에 구현했던 콘솔과 위에서 구현한 텍스트 윈도우를 합쳐놓으면 터미널이 된다.

Terminal 클래스 #

 1class Terminal {
 2 public:
 3  static const int kRows = 15, kColumns = 60;
 4  static const int kLineMax = 128;
 5
 6  Terminal();
 7  unsigned int LayerID() const { return layer_id_; }
 8  // 커서를 깜빡이고 커서 위치 리턴
 9  Rectangle<int> BlinkCursor();
10  // 현재 커서에 글자를 쓰고 쓴 커서 위치 리턴
11  Rectangle<int> InputKey(uint8_t modifier, uint8_t keycode, char ascii);
12
13 private:
14  // 윈도우를 만들어 저장하고 레이어 ID를 관리한다.
15  std::shared_ptr<ToplevelWindow> window_;
16  unsigned int layer_id_;
17
18  // 커서관련 멤버들. 현재 커서를 계산하고 깜빡이게 출력하는 용도. 
19  Vector2D<int> cursor_{0, 0};
20  bool cursor_visible_{false};
21  void DrawCursor(bool visible);
22  Vector2D<int> CalcCursorPos() const;
23
24  int linebuf_index_{0};
25  // 한 라인에 대한 문자열을 저장하는 버퍼. 출력만 할땐 필요없다.
26  // 나중에 한줄에 표시한 문자를 파싱해서 어떤 작업을 하기 위해 추가
27  std::array<char, kLineMax> linebuf_{};
28  // 한 라인을 스크롤하는 함수
29  void Scroll1();
30};
31
32// 터미널의 태스크함수
33void TaskTerminal(uint64_t task_id, int64_t data);

키 입력 #

키를 입력받고 문자가 그려진 위치를 리턴해준다. 나중에 이걸로 메인스레드에 메시지를 보내서 해당 위치를 다시 그리는 작업을 요청한다.

 1Rectangle<int> Terminal::InputKey(
 2    uint8_t modifier, uint8_t keycode, char ascii) {
 3  // 글을 작성하는 동안은 커서가 숨겨짐
 4  DrawCursor(false);
 5  Rectangle<int> draw_area{CalcCursorPos(), {8*2, 16}};
 6
 7  // 입력받은 문자가 개행인 경우 그에맞게 처리. y값을 이동시키거나 Scroll1 함수 호출
 8  if (ascii == '\n') {
 9    linebuf_[linebuf_index_] = 0;
10    linebuf_index_ = 0;
11    cursor_.x = 0;
12    Log(kWarn, "line: %s\n", &linebuf_[0]);
13    if (cursor_.y < kRows - 1) {
14      ++cursor_.y;
15    } else {
16      // 맨 아래 열인데, 개행을 입력받은 경우 전체 픽셀버퍼를 한줄 위로 올려야한다.
17      Scroll1();
18    }
19    draw_area.pos = ToplevelWindow::kTopLeftMargin;
20    draw_area.size = window_->InnerSize();
21  } else if (ascii == '\b') {
22    // 백스페이스를 입력받은 경우 커서를 이동시키고 지운다.
23    if (cursor_.x > 0) {
24      --cursor_.x;
25      FillRectangle(*window_->Writer(), CalcCursorPos(), {8, 16}, {0, 0, 0});
26      draw_area.pos = CalcCursorPos();
27      if (linebuf_index_ > 0) {
28        --linebuf_index_;
29      }
30    }
31  } else if (ascii != 0) {
32    // 나머지의 경우 그냥 한 문자를 쓴다. 
33    if (cursor_.x < kColumns - 1 && linebuf_index_ < kLineMax - 1) {
34      linebuf_[linebuf_index_] = ascii;
35      ++linebuf_index_;
36      WriteAscii(*window_->Writer(), CalcCursorPos(), ascii, {255, 255, 255});
37      ++cursor_.x;
38    }
39  }
40  // 카서를 보이게 표시하고 문자열이 그려진 위치 리턴
41  DrawCursor(true);
42  return draw_area;
43}

한줄 스크롤 #

콘솔에서 구현했던 것 처럼 커서가 맨 아래에 있을때 개행을 받은 경우 전체 터미널 글자 영역을 한줄 위로 올리고 마지막줄은 콘솔 배경색으로 칠한다.

1void Terminal::Scroll1() {
2  Rectangle<int> move_src{
3    ToplevelWindow::kTopLeftMargin + Vector2D<int>{4, 4 + 16},
4    {8*kColumns, 16*(kRows - 1)}
5  };
6  window_->Move(ToplevelWindow::kTopLeftMargin + Vector2D<int>{4, 4}, move_src);
7  FillRectangle(*window_->InnerWriter(),
8                {4, 4 + 16*cursor_.y}, {8*kColumns, 16}, {0, 0, 0});
9}

태스크 함수와 메시지 처리 #

태스크 함수부분 #

터미널을 생성하고, 메시지를 계속 받아서 처리한다.

 1// layer_id 와 task_id 를 매핑시켜놓은 맵
 2extern std::map<unsigned int, uint64_t>* layer_task_map;
 3
 4void TaskTerminal(uint64_t task_id, int64_t data) {
 5  __asm__("cli");
 6  Task& task = task_manager->CurrentTask();
 7  Terminal* terminal = new Terminal;
 8  layer_manager->Move(terminal->LayerID(), {100, 200});
 9  active_layer->Activate(terminal->LayerID());
10  // 생성한 태스크를 레이어와 연결해준다.
11  layer_task_map->insert(std::make_pair(terminal->LayerID(), task_id));
12  __asm__("sti");
13
14  while (true) {
15    __asm__("cli");
16    auto msg = task.ReceiveMessage();
17    if (!msg) {
18      // 메시지가 들어올때까지 슬립. 메시지를 보내면서 Wakeup 된다. 
19      task.Sleep();
20      __asm__("sti");
21      continue;
22    }
23
24    switch (msg->type) {
25    case Message::kTimerTimeout:
26      {
27        // Timeout메시지는 메인스레드(루프)에서 보내준다.
28        // 이 타임아웃은 커서타이머의 타임아웃이라서 깜빡일 타이밍을 알려준것이다.
29        // 보내주면 커서깜빡이는 영역을 알려줘서 그려달라고 다시 요청
30        const auto area = terminal->BlinkCursor();
31        Message msg = MakeLayerMessage(
32            task_id, terminal->LayerID(), LayerOperation::DrawArea, area);
33        __asm__("cli");
34        task_manager->SendMessage(1, msg);
35        __asm__("sti");
36      }
37      break;
38    case Message::kKeyPush:
39      {
40        // 터미널에 입력받은 문자를 그리고 위치를 리턴받음
41        const auto area = terminal->InputKey(msg->arg.keyboard.modifier,
42                                             msg->arg.keyboard.keycode,
43                                             msg->arg.keyboard.ascii);
44        // 해당 영역만 백버퍼에 업데이트하기 위해 DrawArea 메시지 전달
45        Message msg = MakeLayerMessage(
46            task_id, terminal->LayerID(), LayerOperation::DrawArea, area);
47        __asm__("cli");
48        task_manager->SendMessage(1, msg);
49        __asm__("sti");
50      }
51      break;
52    default:
53      break;
54    }
55  }
56}

메인스레드(루프) 부분 #

터미널이 커서를 그려야하기 때문에 TextboxCursorTimer가 타임아웃된 경우 메시지를 전달해서 알려준다. 그리고 입력받은 키를 활성화된 터미널에 전달한다.

 1    case Message::kTimerTimeout:
 2      if (msg->arg.timer.value == kTextboxCursorTimer) {
 3        // ...
 4        __asm__("cli");
 5        // 커서 타이머가 타임아웃되면 task에 직접 알려준다.
 6        task_manager->SendMessage(task_terminal_id, *msg);
 7        __asm__("sti");
 8      }
 9      break;
10    case Message::kKeyPush:
11      if (auto act = active_layer->GetActive(); act == text_window_layer_id) {
12        // ...
13      } else {
14        __asm__("cli");
15        // 활성화된 레이어 ID로 태스크 ID를 찾아온다.
16        auto task_it = layer_task_map->find(act);
17        __asm__("sti");
18        if (task_it != layer_task_map->end()) {
19          __asm__("cli");
20          // 활성화된 태스크에 메시지를 전송한다. 
21          task_manager->SendMessage(task_it->second, *msg);
22          __asm__("sti");
23        } else {
24          // 활성화된 태스크가 없다면 그냥 콘솔에 출력
25          printk("key push not handled: keycode %02x, ascii %02x\n",
26            msg->arg.keyboard.keycode,
27            msg->arg.keyboard.ascii);
28        }
29      }
30      break;

f66af582-5da5-4cba-8663-07c335768661


comments powered by Disqus